Skip to main content
Saved
Foundation

Performance as a Foundation

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

Den Odell
By Den Odell Added
In this article
  1. TL;DR
  2. The Retrofit Trap
  3. Defining Performance
  4. Loading Performance
  5. Runtime Performance
  6. Performance as Architecture
  7. Architectural Decisions That Determine Performance
  8. The Performance Budget
  9. What to Budget
  10. How to Enforce
  11. Perceived Performance
  12. Show Progress
  13. Reduce Perceived Waiting
  14. The Critical Rendering Path
  15. The Path to First Render
  16. JavaScript: The Biggest Lever
  17. The Cost of JavaScript
  18. Managing JavaScript
  19. Server-Side Rendering and Beyond
  20. Client-Side Rendering (CSR)
  21. Server-Side Rendering (SSR)
  22. Images and Media
  23. Format and Size
  24. Layout Stability
  25. Measuring Performance
  26. Lab Data (Synthetic Testing)
  27. Field Data (Real User Monitoring)
  28. Performance Culture
  29. Building Performance Into Process
  30. The Path Forward
  31. Summary
TL;DR

TL;DR

Performance is an architectural constraint that shapes every decision from data fetching to component structure to deployment strategy, so it can’t wait until the end.

Core principles:

Performance is architecture: decisions on day one determine what’s achievable later
Measure real users: field metrics tell you more than synthetic benchmarks
Perceived speed counts: users judge how fast the app feels, not the numbers behind it
Budgets: set limits and enforce them in CI

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 DevTools and finds a 3MB JavaScript bundle, a waterfall of blocking requests, and layout shifts that make the page jump around as it loads.

“We’ll optimize it later,” someone said six months ago. Now “later” has arrived, and optimization means rewriting core architecture.

Performance is hard to retrofit because the slow parts are built into your architecture. The decisions you made about data fetching, component boundaries, and bundle structure 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, then find they’ve built an app that runs slowly.

The fix is to treat performance as part of the architecture from the start, rather than reaching for better optimization techniques later.

Defining Performance

Performance is a collection of metrics rather than a single number, and together they describe how users experience your application.

The Metrics That Matter

Loading Performance

First Contentful Paint (FCP): When the first content appears. The user is no longer looking at a blank screen.

Largest Contentful Paint (LCP): When the main content is visible. Users can start reading or using what they came for.

Total Blocking Time (TBT): The total time long tasks keep the main thread busy while the page loads. Taps and clicks during that time have to wait.

Runtime Performance

Interaction to Next Paint (INP): How responsive the page is throughout its lifecycle. Users see a quick response when they click, tap, or type.

Cumulative Layout Shift (CLS): How stable the visual layout is. Content doesn’t move as the user goes to click, so they don’t click the wrong thing.

LCP, INP, and CLS are the three Core Web Vitals, Google’s attempt to put numbers on 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. Metrics stand in for the experience, and the experience is what we're trying to improve.

You can improve the metrics while making the experience worse, and the reverse. A skeleton screen doesn’t speed up data loading, but it makes the wait feel predictable instead of frustrating, even though the numbers don’t change.

In the end, performance is about respecting users’ time and attention.

Performance as Architecture

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

Architectural Decisions That Determine Performance

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. The shape of your component tree constrains what optimizations are possible.

Your rendering approach determines when users see content. Client-side rendering means waiting for JavaScript to download, parse, and execute before anything appears. Server-side rendering means content arrives with the HTML. That’s an architectural choice, not something you change 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 require architectural solutions, not micro-optimizations.

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

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 hard limit on performance metrics, treated like any other project requirement: a limit you enforce rather than a goal you aim for.

Setting Budgets

What to Budget

  • Bundle size: Total JavaScript, per-route JavaScript, CSS
  • Request count: Number of requests for 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

How to Enforce

  • CI checks: Fail builds that exceed budgets
  • Bundle analyzers: Visualize what’s in your bundles
  • Lighthouse CI: Automated performance testing
  • Real user monitoring: Track metrics in production
  • Alerts: Notify when metrics regress

Without enforcement, a budget is only a suggestion, and suggestions get ignored when a deadline is close.

Budgets make trade-offs visible. When a new feature would push the bundle over budget, the team must choose: optimize existing code, remove something else, or knowingly exceed the budget. Without a budget, that feature ships and the bundle grows without anyone noticing.

Every byte adds to the time users spend waiting. Budgets make you account for that time instead of ignoring it.

Setting initial budgets means knowing 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 don’t notice milliseconds. They notice whether a moment feels fast, normal, or frustratingly slow, and improving perceived performance means changing how those moments feel.

Techniques for Perceived Speed

Show Progress

Skeleton screens show the shape of incoming content, turning a blank wait into visible progress. Users know what’s coming and feel the page is working.

Progress indicators communicate 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. Preloading the next page on hover means it can appear almost straight away on click, so navigation feels fast because the app correctly guessed what the user would open next.

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 every action immediately. A button that reacts when it’s pressed, rather than when the work completes, avoids the pause that users read as lag.

The goal is for things to feel fast as well as be fast. Sometimes that takes real speed improvements, and sometimes it takes better communication 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 a frozen interface 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

Every resource on this path delays rendering. The goal is making the path as short as possible: minimal blocking resources, critical CSS inlined, scripts deferred, everything else loaded after first paint.

This affects architecture. Stylesheets in <head> block rendering, which is why critical CSS is often inlined. 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 matters more for JavaScript than for other assets. Every kilobyte of JavaScript adds processing time that doesn’t show up in transfer size metrics.

Managing JavaScript

Ship less JavaScript. Every dependency is worth questioning. An animation library might be convenient, but CSS may be able to do the same job. A utility library saves time, but tree-shaking might not remove the parts you don’t use. The fastest JavaScript is the JavaScript you don’t ship.

Split your bundles. Users visiting the homepage don’t need the settings page code. Route-based code splitting ensures users only download JavaScript for what they’re using. Dynamic imports enable splitting at any boundary.

Defer what you can. Not all JavaScript needs to run immediately. Analytics, below-the-fold interactivity, and third-party widgets can all load after the critical JavaScript.

Before asking how to optimize your JavaScript, ask how much of it you need. Starting from zero and adding only what's essential keeps that amount small.

Server-Side Rendering and Beyond

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

Rendering Strategies

Client-Side Rendering (CSR)

Browser downloads HTML shell, then JavaScript, then JavaScript fetches data and renders content.

Tradeoffs:

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

Server-Side Rendering (SSR)

Server renders HTML with content, browser displays it as soon as it arrives, JavaScript hydrates for interactivity.

Tradeoffs:

  • 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. A good fit for content that doesn’t change per request.

Incremental Static Regeneration (ISR) adds on-demand regeneration to static pages, balancing freshness with performance.

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

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

Modern meta-frameworks like Next.js, Nuxt, React Router (formerly Remix), and SvelteKit support multiple strategies, sometimes on the same page. So the decision is less about picking one strategy and more about knowing which one 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: Serve different sizes for different screens
  • Lazy loading: Don’t load images until they’re near the viewport
  • Compression: Optimize quality vs. size for each image’s purpose

Layout Stability

  • Explicit dimensions: Always specify width and height
  • Aspect ratio boxes: Reserve space before images load
  • Placeholders: Show blur-up or color placeholders while loading
  • Priority hints: Tell the browser which images matter most

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

Measuring Performance

You need measurements to improve performance, 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. Lighthouse, WebPageTest, local profiling.

Strengths:

  • Reproducible results
  • Debug specific issues
  • Test before deployment
  • Detailed diagnostics

Weaknesses:

  • Doesn’t reflect real users
  • Miss real-world conditions
  • Can be gamed

Field Data (Real User Monitoring)

Metrics from real users on real devices and networks. Core Web Vitals, custom metrics.

Strengths:

  • Reflects real experience
  • Captures device diversity
  • Shows geographic variation
  • Reveals edge cases

Weaknesses:

  • Harder to debug
  • Can’t test unreleased code
  • Requires scale for significance

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 numbers are the ones to improve.

Performance Culture

Performance is an ongoing practice the whole team keeps up, rather than a one-time project. Teams often get performance into good shape once, then lose it within months as features pile up and nobody checks their effect on speed.

Building Performance Into Process

Make performance visible. Real user metrics go on a dashboard the team can see, improvements get celebrated, and regressions get investigated. When performance is visible, it stays on the radar.

Treat regressions as bugs. If a deploy increases LCP by 500ms, that’s a bug rather than a trade-off to accept, and it should block a release the same way a functional bug would.

Review for performance. Code review should include performance considerations. Is this new dependency necessary? Could this component be code-split? Will this pattern cause re-renders?

Test on real devices. Budget Android phones kept in the office and used regularly show the team what many users experience. The difference between a developer’s MacBook and a typical user’s device is large, and easy to miss until you use the slower one yourself.

Performance culture means asking 'how will this affect performance?' as naturally as asking 'does this work?' That way it's part of how the team thinks rather than a separate concern.

The Path Forward

Treating performance as a foundation means treating speed as a constraint from the beginning, instead of something to optimize afterward, because it shapes the architecture.

The principles are straightforward:

  1. Set budgets early and enforce them in CI
  2. Choose rendering strategies based on performance needs
  3. Ship minimal JavaScript and split what you must ship
  4. Measure real users on real devices
  5. Make trade-offs explicit when performance conflicts with features

Building this way takes less work than retrofitting, gives users a better experience, and respects their time. Performance is part of whether your application works, so it can't be optional.

Every delay gets in the way of what your users are trying to do, and building performance in from the start keeps those delays down by design.

Summary

  1. Performance is architectural. Decisions on day one determine what’s possible later
  2. Set and enforce performance budgets, treating them as requirements rather than goals
  3. Perceived performance matters as much as measured performance
  4. JavaScript usually has the biggest effect: ship less, split more, and defer what you can
  5. Choose rendering strategies (CSR/SSR/SSG) based on performance needs
  6. Measure real users on real devices as well as running synthetic tests
  7. Build performance into culture through visibility, reviews, and treating regressions as bugs

Related Content:

External Resources:

Newsletter

A Monthly Email
from Den Odell

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

No spam. Unsubscribe anytime.