Skip to main content
Saved
Foundation 14 min read

Performance as a Foundation

Performance belongs in the architecture from day one, instead of being bolted on after users start complaining.

TL;DR

TL;DR

Performance is an architectural constraint. It affects your decisions about everything from data fetching to deployment, so you can’t leave it until the end.

Core principles:

The decisions you make on day one limit how fast your app can be later
Measurements from real users tell you more than synthetic benchmarks do
Users judge your app by how fast it feels, so perceived speed counts too
A performance budget only works when your CI pipeline enforces it

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

Donald Knuth

The Retrofit Trap

I’ve watched teams build applications for months, shipping features quickly and celebrating how fast they’re moving, until someone notices the app feels slow and users start complaining. Someone opens the browser’s developer tools and finds a 3MB JavaScript bundle and a waterfall of blocking requests.

“We’ll optimize it later,” someone said six months ago. Now “later” has arrived, and the only way to make the app faster is to rewrite its core architecture.

Performance is hard to retrofit because the slow parts are built into your architecture. The decisions you made about data fetching and component boundaries now limit what you can change.

I’ve seen the same thing happen at startups and at large companies, in teams that treat performance as an optimization rather than a constraint. They ship quickly, and then they find out the app they’ve built is slow. If your team works that way, you might not find out until your users tell you.

You avoid this by treating performance as part of your architecture from the start, instead of hoping to find better optimization techniques later.

Defining Performance

There isn’t one number that tells you how fast your app is. There are several metrics, and together they describe how your users experience it.

The Metrics to Watch

Loading
FCP First Contentful Paint
When the first content appears. The user is no longer looking at a blank screen.
Loading
LCP Largest Contentful Paint
When the main content is visible. Users can start reading or using what they came for.
Under 2.5s
Loading
TBT Total Blocking Time
The total time long tasks keep the main thread busy while the page loads. Taps and clicks during that time have to wait.
Runtime
INP Interaction to Next Paint
How responsive the page is throughout its lifecycle. Users see a quick response when they click, tap, or type.
Under 200ms
Runtime
CLS Cumulative Layout Shift
How stable the visual layout is. Content doesn't move as the user goes to click, so they don't click the wrong thing.
Under 0.1

Highlighted: the three Core Web Vitals, with the targets from the budgets later in this article.

LCP, INP, and CLS are the three Core Web Vitals, Google’s attempt to measure user experience. The metrics are useful because they correlate with how users feel when using your application.

A user doesn't know their LCP was 2.4 seconds. They know the page felt fast or felt slow. The metrics are a way of measuring that experience, and it's the experience we're trying to improve.

You can improve the metrics while making the experience worse, and you can make the experience better without changing the metrics. A skeleton screen doesn’t speed up data loading, but it makes the loading time feel predictable instead of frustrating.

Performance as Architecture

Architectural Decisions That Determine Performance

Performance can't be bolted on because it's determined by architectural decisions made early in development.

Your data fetching strategy determines whether pages load quickly or slowly. Fetching data on the client after JavaScript executes is fundamentally slower than fetching on the server before HTML is sent. Optimizing the client code doesn’t change that, because the request still can’t start until the JavaScript has run.

Your component structure determines bundle size and code-splitting opportunities. Components that import everything can’t be split effectively. Dependencies that cross feature boundaries prevent lazy loading.

Your rendering approach determines when users see content. With client-side rendering, users wait for JavaScript to download, parse, and execute before anything appears. With server-side rendering, the content arrives with the HTML. You make that choice when you design the architecture, and you can’t change it later with a config flag.

Your state management determines how efficiently updates propagate. Global state that triggers re-renders across unrelated components creates performance problems that need architectural fixes rather than micro-optimizations.

When someone asks 'can we make this faster?' the honest reply is often 'not without changing how it's built.' That's why performance constraints belong in the architecture from day one.

If you wait until the end to think about performance, the architecture is already set, and you’re left with small optimizations that can’t undo the big decisions.

The Performance Budget

A performance budget is a set of hard limits for your performance metrics. Your team enforces those limits the same way it enforces any other project requirement.

Setting Budgets

Metrics to Budget

  • Bundle size: Total JavaScript, per-route JavaScript, and CSS
  • Request count: Number of requests for the initial load
  • Core Web Vitals: LCP under 2.5s, INP under 200ms, CLS under 0.1
  • Image weight: Total bytes of images per page
  • Third-party scripts: Bytes and blocking time from external code

Ways to Enforce

  • CI checks: The build fails when the bundle goes over budget
  • Bundle analyzers: Show you what’s in your bundles
  • Lighthouse CI: Runs performance tests automatically
  • Real user monitoring: Tracks metrics in production
  • Alerts: Let the team know when a metric gets worse

If nothing enforces the budget, the team will probably ignore it when a deadline is close.

When a new feature would push your bundle over budget, you have to choose between optimizing existing code, removing something else, or going over the budget knowingly. Without a budget, the feature would ship and the bundle would grow, and the team probably wouldn’t notice.

Every byte adds to the time users spend waiting. With a budget in place, the team has to decide whether each addition is worth that time.

To set your initial budgets, you need to know what devices and networks your users have. A budget that suits users on fiber with flagship phones is far too generous for users on 3G with budget Android devices.

Perceived Performance

Users notice whether a moment feels fast or frustratingly slow, more than they notice the milliseconds, so to improve perceived performance, you change how those moments feel.

Techniques for Perceived Speed

Show Progress

Skeleton screens show the layout of the content before it arrives, so users see progress instead of a blank screen. They know what’s coming and feel the page is working.

Progress indicators show that something is happening. A determinate progress bar is better than a spinner, and a spinner is better than nothing, because users read no feedback at all as a sign that something is broken.

Optimistic updates show results before confirmation. When a user likes a post, it shows as liked straight away, and rolls back if the server rejects it. The instant feedback feels responsive even when the network is slow.

Reduce Perceived Waiting

Preloading fetches resources before they’re needed. If you preload the next page when the user hovers over a link, it can appear almost straight away when they click, so navigation feels fast.

Progressive loading shows content as it arrives. Text arrives before images, and low-resolution images before high-resolution ones, so the user has something to look at sooner.

Instant feedback acknowledges each action as it happens. A button that reacts when it’s pressed, rather than when the work completes, avoids the pause that users read as lag.

You want your app to feel fast as well as be fast. Sometimes that takes real speed improvements, and sometimes it takes better feedback about what's happening.

Animation helps here too. I’ve noticed that a 300ms animation that plays while content loads feels like a designed transition, while the same 300ms with an interface that doesn’t respond feels like lag.

The Critical Rendering Path

Every web page has a critical rendering path: the steps from receiving HTML to displaying usable content. Knowing what’s on this path tells you what’s delaying the first render.

The Path to First Render

  1. HTML parsing begins: The browser reads HTML and builds the DOM
  2. CSS blocks rendering: Stylesheets must download and parse before layout
  3. JavaScript can block everything: Scripts in <head> without async or defer stop parsing
  4. Layout calculates positions: Once DOM and CSSOM are ready, layout begins
  5. Paint shows pixels: Finally, content appears on screen

Each resource on this path delays rendering, so you want the path to be as short as possible, with few blocking resources, critical CSS inlined, scripts deferred, and everything else loaded after first paint.

Stylesheets in <head> block rendering, which is why critical CSS is often inlined, and scripts without defer block parsing, so they get defer or move to the end of the body. Third-party scripts add unpredictable blocking time, so they’re best loaded asynchronously, or not at all.

Every resource you add to the critical path delays first paint, and most things can wait until after it.

JavaScript: The Biggest Lever

In most frontend apps, JavaScript has the biggest effect on performance, and it’s where many performance problems start.

The Cost of JavaScript

JavaScript takes more work for the browser than other assets. Unlike images, which download and display, JavaScript must download, parse, compile, and execute. Each step takes time, especially on mobile devices with slower CPUs.

A 500KB image is a 500KB download. A 500KB JavaScript bundle is a 500KB download plus the CPU time to parse, compile, and execute it. On a budget Android phone, that processing can take longer than the download itself.

This is why bundle size is a bigger problem for JavaScript than for other assets. Every kilobyte of JavaScript adds processing time that doesn’t show up in transfer size metrics.

Managing JavaScript

You can ship less JavaScript by questioning each dependency you add. An animation library might be convenient, but CSS may be able to do the same job. A utility library saves you time, but tree-shaking might not remove the parts you don’t use.

If you split your bundles, users visiting the homepage don’t have to download the settings page code. With route-based code splitting, users only download JavaScript for the pages they’re using, and dynamic imports let you split at any boundary.

It also helps to defer what you can, because not all JavaScript needs to run immediately. Analytics and third-party widgets can both load after the critical JavaScript.

Before you think about how to optimize your JavaScript, it's worth asking how much of it you need. If you start from zero and only add what's essential, that amount stays small.

Server-Side Rendering and Beyond

Where and when your code runs has a large effect on performance.

Rendering Strategies

Client-Side Rendering (CSR)

The browser downloads the HTML shell, then the JavaScript, and then the JavaScript fetches data and renders the content.

Trade-offs:

  • Slowest first paint
  • Blank screen until JavaScript runs
  • Poor SEO without additional work
  • Simplest deployment, as static files

Server-Side Rendering (SSR)

The server renders HTML with the content, the browser displays it as soon as it arrives, and JavaScript hydrates it for interactivity.

Trade-offs:

  • Fastest first paint
  • Content visible before JavaScript
  • SEO-friendly out of the box
  • Requires server infrastructure

Static Site Generation (SSG) pre-renders pages at build time, combining SSR’s fast first paint with CSR’s simple deployment. It’s a good fit for content that doesn’t change between requests.

Incremental Static Regeneration (ISR) adds on-demand regeneration to static pages, so you can keep content fresh without giving up the speed of static files.

Streaming SSR sends HTML progressively as it’s generated, which improves Time to First Byte and lets content appear before the full page is ready.

These are architectural decisions as well as deployment options, and they determine what performance is achievable, so it's worth making them early and on purpose.

Modern meta-frameworks like Next.js, Nuxt, SvelteKit, and React Router, which used to be Remix, support multiple strategies, sometimes on the same page. So you don’t have to pick one strategy for your whole app. You can choose the one that fits each use case.

Images and Media

After JavaScript, images are usually the biggest cause of slow loading. Unlike JavaScript, they don’t block rendering, but they use bandwidth and can cause layout shifts.

Image Optimization

Format and Size

  • Modern formats: WebP and AVIF offer 25-50% smaller files than JPEG
  • Responsive images: Different sizes for different screens
  • Lazy loading: Images only load when they’re near the viewport
  • Compression: The right balance of quality and size for what each image is used for

Layout Stability

  • Explicit dimensions: A width and height set on each image
  • Aspect ratio boxes: Space reserved before images load
  • Placeholders: Blur-up or color placeholders while images load
  • Priority hints: Telling the browser which images to load first

The <picture> element and srcset attribute enable responsive images, and loading="lazy" defers offscreen images. The fetchpriority attribute tells the browser how urgent each image is. These are HTML features rather than JavaScript libraries, so the browser knows about them and can optimize for them.

Measuring Performance

Improving performance starts with measurements, and the two main kinds tell you different things.

Lab Data vs. Field Data

Lab Data Synthetic Testing Controlled tests on known devices and networks, using tools like Lighthouse and WebPageTest. Field Data Real User Monitoring Metrics from real users on real devices and networks, including Core Web Vitals and custom metrics.
Strengths
  • Reproducible results
  • Useful for debugging specific issues
  • Runs before deployment
  • Detailed diagnostics
  • Reflects real experience
  • Captures device diversity
  • Shows geographic variation
  • Reveals edge cases
Weaknesses
  • Doesn't reflect real users
  • Misses real-world conditions
  • Can be gamed
  • Harder to debug
  • Can't test unreleased code
  • Needs a lot of traffic to be significant

You need both, for different jobs: lab data helps you diagnose and debug, and field data tells you whether users see the improvements. A Lighthouse score of 100 doesn’t help if real users on real devices see something different.

Lighthouse scores help developers find problems, while field Core Web Vitals show what users experience. The field metrics are the ones to improve.

Performance Culture

Performance is something the whole team keeps working at. You might make your app fast once, and then find it’s slow again a few months later, because features kept piling up and the team stopped checking how each one affected speed.

Building Performance Into Process

It helps to make performance visible, with real user metrics on a dashboard the team can see, and someone who investigates each regression.

Your team can treat regressions as bugs. If a deploy increases LCP by 500ms, that should block the release the same way a functional bug would.

Performance can be part of the code review too, with reviewers asking whether a new dependency is necessary, or whether a pattern will cause extra re-renders.

Testing on real devices shows the team what many users experience, and a few budget Android phones in the office make that easy. A developer’s MacBook is much faster than a typical user’s phone, and it’s easy to forget that until you use the slower one yourself.

In a performance culture, your team asks 'how will this affect performance?' as naturally as it asks 'does this work?' Performance becomes part of how the team thinks about each change.

The Path Forward

When you treat performance as a foundation, you treat speed as a constraint from the beginning, because it shapes your architecture. In practice, that comes down to five things:

  1. Performance budgets, set early and enforced in CI
  2. Rendering strategies chosen with performance in mind
  3. Minimal JavaScript, with what you do ship split into smaller bundles
  4. Measurements from real users on real devices
  5. Deliberate trade-offs when performance conflicts with a feature

Building this way takes less work than retrofitting, and your users get a better experience. If your app is too slow to use, it doesn't work, so performance can't be optional.

Each delay gets in the way of what your users are trying to do, and building performance in from the start keeps those delays short.

Summary

  1. Performance is architectural, and the decisions you make on day one limit what’s possible later
  2. Performance budgets are requirements your team enforces
  3. Perceived performance counts as much as measured performance
  4. JavaScript usually has the biggest effect, so it helps to ship less, split more, and defer what you can
  5. Rendering strategies such as CSR, SSR, and SSG are best chosen based on performance needs
  6. You need measurements from real users on real devices as well as synthetic tests
  7. Performance becomes part of your team’s culture through visibility, code reviews, testing on real devices, and treating regressions as bugs

Related Content:

External Resources:

Next Foundation · 13 min read

Resilience by Design

Systems keep working when things fail if you design for failure and recovery from the start, instead of adding error handling at the end.

Read the next Foundation →

Newsletter

A monthly email
from Den Odell.

Behind-the-scenes thinking on frontend patterns, site updates, and more.

No spam. Unsubscribe anytime.