Performance Foundations
(Making speed a feature-level responsibility)
Senior rule:
Performance is not something you sprinkle on after the UI is done.
In Atlas, users open the dashboard many times a day. Slow first load damages trust. Slow filters damage productivity. Unstable layout makes charts feel broken. Senior frontend engineers treat these as product correctness issues.
Conceptual Model
Performance has three layers:
| Layer | Question |
|---|---|
| Loading | How quickly does useful content appear? |
| Interaction | How quickly does the UI respond after input? |
| Stability | Does the page move, block, or surprise users while work happens? |
Core signals:
- LCP: main content visibility.
- INP: interaction responsiveness.
- CLS: layout stability.
- TTFB: server and network responsiveness.
- JavaScript cost: parse, compile, execute, hydrate, and re-render.
Resource Loading
Senior engineers classify resources before optimizing them:
| Resource | Example in Atlas | Loading strategy |
|---|---|---|
| Critical CSS | dashboard shell layout | render-blocking but minimal |
| LCP image or hero chart shell | dashboard overview | preload only if truly critical |
| Below-fold widgets | secondary reports | lazy load |
| Likely next route | report detail page | prefetch after idle or intent |
| Cross-origin API | analytics domain | preconnect if high confidence |
// app/dashboard/layout.tsx
import Script from "next/script";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Script
src="https://analytics.example.com/client.js"
strategy="afterInteractive"
/>
</>
);
}
Use beforeInteractive only for scripts that must run before the app becomes interactive, such as consent or bot protection. Most product analytics should not compete with the first meaningful render.
Images and Layout Stability
import Image from "next/image";
export function CustomerLogo() {
return (
<Image
src="/customers/acme.png"
alt="Acme"
width={96}
height={32}
sizes="96px"
priority={false}
/>
);
}
Senior habits:
- give images dimensions
- avoid lazy-loading the LCP image
- use responsive sizes for grid images
- avoid shipping 2MB images into 96px containers
- reserve space for charts and tables before data arrives
Code Splitting and Dynamic Imports
Atlas has a heavy cohort chart used by only one tab. Do not ship it in the initial dashboard bundle.
import dynamic from "next/dynamic";
const CohortChart = dynamic(() => import("./CohortChart"), {
loading: () => <div className="chart-skeleton" aria-label="Loading chart" />,
});
export function CohortTab() {
return <CohortChart />;
}
Code splitting helps transfer and execution cost, but it can create loading gaps. Pair dynamic imports with stable skeletons and clear boundaries.
Compression and Minification
Compression and minification solve different problems:
| Technique | Reduces | Does not reduce |
|---|---|---|
| Minification | source text size | runtime complexity |
| gzip or Brotli | transfer size | parse and execution cost |
| Tree shaking | unused module code when statically detectable | side-effectful dependencies |
Senior engineers check both network transfer and runtime execution. A minified 400KB chart library can still block the main thread after it arrives.
Virtualization
Large dashboard tables should not render thousands of rows.
type Row = { id: string; customer: string; revenue: number };
export function VisibleRows({
rows,
start,
end,
}: {
rows: Row[];
start: number;
end: number;
}) {
return (
<tbody>
{rows.slice(start, end).map((row) => (
<tr key={row.id}>
<td>{row.customer}</td>
<td>{row.revenue.toLocaleString()}</td>
</tr>
))}
</tbody>
);
}
In production, use a proven virtualization library for complex tables. The senior skill is knowing when DOM size has become a performance problem and ensuring accessibility is not lost.
Caching
Performance caching is a contract about freshness.
// Server Component fetch with explicit revalidation intent.
async function getDashboardSummary() {
const response = await fetch("https://api.example.com/dashboard/summary", {
next: { revalidate: 60 },
});
if (!response.ok) throw new Error("Failed to load dashboard summary");
return response.json() as Promise<DashboardSummary>;
}
Ask:
- who owns freshness?
- what invalidates the data?
- what stale state is acceptable?
- can the UI say "showing cached data" when needed?
Third-Party Script Budget
Before adding a script to Atlas:
type ThirdPartyScriptReview = {
name: string;
owner: string;
purpose: string;
loadingStrategy: "beforeInteractive" | "afterInteractive" | "lazyOnload";
consentRequired: boolean;
expectedRoutes: string[];
rollbackPlan: string;
};
Scripts need owners because they affect performance, privacy, reliability, and debugging.
Mini Case Study: The Dashboard Got Slower After Analytics
Atlas added a session replay tool. LCP barely changed, but INP worsened on low-end Android devices.
Fix:
- load the script after interaction
- disable replay on low-value routes
- sample sessions
- defer initialization until consent is known
- track long tasks by route and script version
The issue was not the tool alone. The issue was ungoverned runtime cost.
Common Failure Modes
- Lazy-loading the LCP image.
- Splitting code so aggressively that every interaction waits.
- Treating compression and minification as substitutes for removing code.
- Measuring only Lighthouse while ignoring real-user data.
- Optimizing transfer size but ignoring JavaScript execution.
- Adding third-party scripts without ownership.
- Rendering large tables without virtualization or pagination.
Review Checklist
- Is the LCP element known?
- Are critical resources prioritized deliberately?
- Are non-critical widgets deferred?
- Are images sized and compressed?
- Are compression, minification, and tree shaking verified in production output?
- Is JavaScript execution measured, not just bundle size?
- Are large lists virtualized or paginated?
- Are third-party scripts reviewed and owned?
- Is production performance segmented by route and device?
Senior Deep Dive: Performance As Product Correctness
A senior frontend engineer does not ask "is the page fast?" as a single question. They break performance into user promises:
| Promise | User interpretation | Engineering signal |
|---|---|---|
| "I can see what I came for" | the page is useful quickly | LCP, TTFB, critical CSS, image priority |
| "The product reacts to me" | taps, typing, filters, and drag actions feel immediate | INP, long tasks, React commit time, event timing |
| "The UI is not lying" | content does not jump, disappear, or show stale certainty | CLS, skeleton stability, stale-state labels |
| "The app respects my device" | it does not drain battery or freeze lower-end phones | JavaScript cost, memory growth, background requests |
| "The next screen is not a reset" | navigation preserves context and feels continuous | route transition time, cache reuse, prefetch usefulness |
This framing matters because senior engineers often inherit vague complaints: "the dashboard feels slow," "filters are laggy," or "mobile is bad." The senior move is to translate those complaints into a performance hypothesis that can be measured and acted on.
The hidden cost stack
The frontend pays for more than transferred bytes:
| Cost | Common source | Senior-level control |
|---|---|---|
| Network round trips | request waterfalls, third-party origins, uncached API calls | route data design, BFF shaping, cache ownership |
| Transfer | images, fonts, bundles, JSON overfetch | compression, responsive media, payload trimming |
| Parse and compile | large JavaScript bundles | dependency review, route splitting, server/client boundaries |
| Execution | hydration, render, analytics, data transforms | main-thread budgeting, idle work, worker/server offload |
| Layout and paint | unbounded DOM, missing dimensions, layout reads/writes | stable dimensions, virtualization, CSS discipline |
| Memory and GC | retained stores, huge lists, cached objects | disposal contracts, bounded caches, profiling |
The weakness this corrects: mid-level engineers often optimize the visible symptom while leaving the cost stack unchanged. A senior engineer asks which layer created the delay and whether the fix reduces actual user wait.
Performance budget design
A useful feature-level budget is specific:
| Budget item | Example senior target |
|---|---|
| Route | /dashboard/reports on mobile and desktop separately |
| User cohort | p75 logged-in users in top three regions |
| Device profile | mid-range Android and normal business laptop |
| Initial JavaScript | route budget plus owner for every large dependency |
| Main-thread work | no interaction-blocking long task during filter apply |
| Media | explicit dimensions, format policy, no unbounded hero/chart assets |
| Data | maximum initial requests and no duplicate route fetches |
| Exception process | owner, expiry date, and product reason |
Budgets are not punishment. They are a way to make tradeoffs visible before a feature quietly adds permanent cost.
Performance review prompts
Use these during PR or design review:
- What is the user's first useful moment?
- What is the largest resource and why is it necessary now?
- Which code runs before the user can interact?
- Which work repeats on every navigation or every keystroke?
- Which dependency was added and what route pays for it?
- Which third-party script runs before consent or before interaction?
- What happens on a mid-range phone with CPU throttling?
- Which production metric will confirm the change was safe?
If the author cannot answer these, the feature is not necessarily wrong, but the performance consequences are not yet understood.
Exercises
Exercise 1 - LCP Audit
Pick a route and identify:
- LCP element
- critical CSS and JS
- critical image or data dependency
- one thing delaying useful content
Exercise 2 - Main Thread Budget
Open DevTools Performance and find the longest task. Classify it as:
- required now
- deferrable
- removable
- movable to a worker
Exercise 3 - Script Review
Choose one third-party script and write a review record with owner, loading strategy, consent, performance risk, and removal path.