Skip to content

Chapter 67: Performance: Measuring What Matters

The platform-first architecture has a strong performance story. The chapter has to make the story specific, with the metrics that matter, the tools that measure them, and the trade-offs the architecture makes that show up in the numbers.

Performance is one of the architecture’s clearest measurable wins. Smaller bundles. Faster initial paint. No hydration cost. Less JavaScript to download, parse, and execute. The wins aren’t theoretical — they show up directly in the Core Web Vitals every modern application is now measured against. This chapter walks through what to measure, how to measure it, what the Kit architecture’s numbers tend to look like, and where the trade-offs make the framework-bound alternative the right choice instead.

The chapter also engages honestly with the React reconciliation argument — the framework’s runtime cost is paid for by faster updates once the application is loaded — and shows where the trade-off cuts in each direction.

Google’s Core Web Vitals, introduced in 2020 and refined since, are the field’s most-cited performance metrics. Three metrics matter for most applications:

Largest Contentful Paint (LCP) — how long until the page’s main visual content appears. The threshold for good is 2.5 seconds; poor is over 4 seconds. LCP measures from when the user starts loading the page until the largest visible content element finishes rendering.

Cumulative Layout Shift (CLS) — how much the page’s visible content shifts during loading. Each shift is scored by the fraction of the viewport affected and the distance moved; CLS accumulates over the page’s loading period. Good is below 0.1; poor is above 0.25. CLS catches the the page jumped while I was about to click class of frustration.

Interaction to Next Paint (INP) — how long the page takes to visibly respond to user interactions. Replaced First Input Delay (FID) in 2024 as the responsiveness metric. Good is under 200ms; poor is over 500ms. INP measures the worst-case response time for the page’s interactions, not just the first one.

Google uses these metrics for search ranking. Most analytics tools and performance-monitoring services report them. PageSpeed Insights, Lighthouse, and the Chrome DevTools Performance panel all measure them.

For the Kit architecture, the metrics typically come out well. LCP is fast because the initial HTML includes the rendered content (server rendering, Chapter 66). CLS is low because the page’s structure is in the HTML from the start; the runtime doesn’t reshape the layout. INP is fast because the runtime is small and the platform’s native controls have native response times.

A typical Kit application might look like:

  • LCP: 1.2–1.8 seconds (depending on network and content).
  • CLS: under 0.05 (the architecture doesn’t introduce layout shifts).
  • INP: 50–150ms (the platform’s native controls are fast; the runtime’s metadata-boundary listener is a constant-time DOM walk).

The numbers aren’t magic. They’re what the architecture produces because the architecture leans on the platform’s existing fast paths.

Lighthouse, built into Chrome DevTools and available as a standalone tool, is the field’s most-used performance audit. The tool runs against a production URL (or a local one), measures the Core Web Vitals plus a longer list of secondary metrics, and produces a score from 0 to 100.

Beyond the Core Web Vitals, Lighthouse measures:

Time to Interactive (TTI) — when the page becomes fully interactive. For SPA-style applications, TTI is often substantially after LCP because hydration has to complete before interactions work.

Total Blocking Time (TBT) — total time the main thread is blocked during loading. High TBT correlates with poor INP later.

Speed Index — visual progress of the page during loading. Captures how quickly the page feels complete.

JavaScript bundle size — the total size of JavaScript shipped to the user. Not technically a metric but reported as part of the audit.

Lighthouse also runs accessibility, SEO, and best-practices audits. The accessibility audit catches structural issues (missing alt text, color contrast, form labels). The SEO audit catches missing meta tags and crawlability problems.

For a Kit application, Lighthouse scores typically land in the 90–100 range across all four categories (Performance, Accessibility, Best Practices, SEO), if the application is built carefully. The platform-first architecture aligns with what Lighthouse rewards — small bundles, fast paint, semantic HTML, accessible controls, predictable layout.

Lighthouse and PageSpeed Insights are lab tests — they run in controlled environments. They don’t reflect what real users on real devices on real networks experience.

Real User Monitoring (RUM) fills the gap. The application includes a small client-side script that measures the Core Web Vitals during real page loads and reports them to a monitoring service. Datadog, New Relic, Speedcurve, Sentry’s performance product, Google Analytics 4, and a dozen other services collect these metrics at scale.

The Web Vitals library (web-vitals on npm, maintained by the Chrome team) is the standard way to collect the metrics. The library is small (under 2 KB) and exposes the Core Web Vitals plus several others:

import { onCLS, onLCP, onINP } from 'web-vitals'
onCLS((metric) => sendToAnalytics(metric))
onLCP((metric) => sendToAnalytics(metric))
onINP((metric) => sendToAnalytics(metric))

The reported metrics include the value (the measured number), the delta (the change from the last report), the id (a unique identifier for the page load), and attribution (which DOM elements caused the metric, for CLS and LCP).

For the Kit architecture, RUM integration is one module:

const performanceModule = defineKitModule({
name: 'performance',
onInstall: async () => {
const { onCLS, onLCP, onINP } = await import('web-vitals')
onCLS(reportMetric)
onLCP(reportMetric)
onINP(reportMetric)
}
})
function reportMetric(metric: any) {
runtime.emit({
type: 'performance.metric',
payload: metric
})
}

The metrics flow through the runtime’s event bus. An analytics module subscribed to performance.metric can forward them to whichever monitoring service the team uses. The pattern composes with the rest of the architecture.

The single most direct performance optimization is ship less JavaScript.

The numbers for a typical React-based meta-framework application:

  • Framework runtime (React + ReactDOM): ~40 KB minified + gzipped.
  • Meta-framework runtime (Next.js or similar): ~50–100 KB.
  • State management library (Redux, Zustand, etc.): ~5–15 KB.
  • Router (React Router, TanStack Router): ~15–30 KB.
  • Component library (Material UI, Chakra, etc.): ~100–300 KB.
  • Application code: variable, typically 100–500 KB.

A reasonable starting point is around 300–600 KB of JavaScript before the application’s own code. Many production React applications ship much more than that.

The Kit architecture:

  • Runtime (Chapters 39–44): under 30 KB.
  • Lit (Chapter 45): under 10 KB.
  • Component library: variable; 30–80 KB for the Kit components from Part V.
  • Application code: variable.

A reasonable Kit application ships around 70–120 KB of JavaScript before the application’s own code. The difference — 200–500 KB less per page — shows up directly in LCP and TTI.

Bundle size isn’t the only performance dimension, but it’s the easiest to measure and one of the most impactful. The user’s device has to download the bundle (network time), parse it (CPU time), compile it (V8 optimization), and execute it (more CPU time). Every kilobyte adds time. The compounding effect on mobile devices or slow networks is substantial.

Hydration vs. Attachment: The Measurable Difference

Section titled “Hydration vs. Attachment: The Measurable Difference”

Chapter 66 introduced the attachment-vs-hydration distinction. The performance implications are worth showing concretely.

A hydrating application has to:

  1. Download the framework runtime.
  2. Download the application’s component code (often the same code the server used to render).
  3. Parse and execute the application code.
  4. Re-render the component tree in memory.
  5. Reconcile the in-memory tree against the server-rendered DOM.
  6. Attach event listeners.
  7. Initialize the framework’s internal state.

The total time is Time to Interactive. For a typical React application, TTI is usually 2–4 seconds after LCP. The page is visually rendered but not interactive — clicks may not work, animations may stutter, scrolling may be janky — until hydration completes.

An attaching application has to:

  1. Download the runtime (small).
  2. Parse and execute the runtime code.
  3. Register custom elements (the browser upgrades existing instances).
  4. Attach the metadata-boundary listener.
  5. Install the application’s modules.

Total time is typically under 500ms after LCP. The page is interactive almost immediately. The custom elements upgrade in place; their server-rendered shadow DOM (with declarative shadow DOM) means there’s no flash of unstyled content.

The difference is measurable. A Kit application’s Total Blocking Time is typically a fraction of an equivalent React application’s. INP stays low because the architecture doesn’t block the main thread with framework reconciliation during interactions.

The React Reconciliation Argument, Honestly

Section titled “The React Reconciliation Argument, Honestly”

The chapter has to engage with React’s strongest performance argument fairly.

React’s defenders argue that the framework’s reconciliation pays for itself once the application is loaded. Updates are fast because React batches them, deduplicates them, and applies only the minimal DOM changes. For complex applications with many state changes, the reconciliation approach is genuinely efficient — possibly more efficient than manual DOM manipulation would be.

The argument is correct for the workloads where it applies. A complex real-time application with hundreds of frequent state changes per second benefits from React’s batching. A Figma-class design tool, a Linear-class issue tracker, a Notion-class document editor — these applications have legitimate use for React’s reconciliation.

For most applications, the argument is less compelling. A typical business application’s state changes are infrequent (every few seconds, in response to user action), localized (one component at a time), and not performance-critical (a 30ms render is invisible to the user). The reconciliation overhead exists but isn’t paying for any meaningful work. The application would feel the same with direct DOM updates.

The Solid and Svelte frameworks (Chapter 14) made similar arguments — fine-grained reactivity or compile-time templating produces better update performance than React’s reconciliation for most workloads. The benchmark numbers consistently support this; the architectural argument is now mainstream.

For the Kit architecture, the position is: most applications don’t need React’s reconciliation, and the cost of paying for it (larger bundle, longer TTI, framework dependency) outweighs the benefit. For applications that do need it, React (or one of the modern alternatives with better reactivity profiles) is the right tool. The choice should be made deliberately, with the trade-offs understood, rather than by default.

A practical pattern worth naming: performance budgets.

A budget is a target the team commits to. JavaScript bundle size under X KB. LCP under Y seconds. INP under Z milliseconds. The budget is enforced through CI tools that fail the build if the budget is exceeded.

Tools that enforce budgets:

  • bundlesize and bundlewatch — check the bundle’s size against a configured limit.
  • Lighthouse CI — run Lighthouse in CI and fail if scores drop below thresholds.
  • Calibre and Speedcurve — third-party services that monitor production performance and alert when budgets are breached.
  • Performance Insights in Chrome DevTools — local analysis, not strictly CI but useful for development.

For the Kit architecture, sensible budgets:

  • Initial JavaScript bundle (runtime + components): under 80 KB gzipped.
  • Total page weight: under 500 KB on initial load.
  • LCP: under 2.0 seconds on a 4G connection.
  • INP: under 200ms on a mid-range Android device.
  • CLS: under 0.05.

The budgets aren’t ambitious; they’re achievable defaults. Most Kit applications hit them without specific optimization work. The budget’s job is to catch regression — when a new dependency is added, when a component grows, when an animation introduces layout shift — before it ships.

A reverence beat the chapter has to land.

Addy Osmani has been on the Chrome team since the early 2010s and has been one of the field’s most prolific performance advocates. His talks, blog posts, books (Image Optimization, Learning JavaScript Design Patterns, Web Performance in Action, and others), and tools have shaped how the working frontend developer thinks about performance.

The Chrome team’s broader performance work — the Core Web Vitals initiative, the Performance API improvements, the Lighthouse tool, the Chrome DevTools Performance panel, the SourceMap improvements, the parsing optimizations in V8 — has been a steady tide raising what the platform is capable of. The work is mostly invisible to application developers; it shows up as the browser got faster, year after year, without the application having to do anything.

Other contributors deserve mention. Houssein Djirdeh, Barry Pollard, Annie Sullivan, Ilya Grigorik — each has been a long-running voice in web performance. The PageSpeed team. The HTTP Archive project. The WebPageTest tool (Patrick Meenan’s long-running gift to the community).

The cumulative effect is that the measurement infrastructure for web performance is excellent. Teams can know precisely how their applications perform, where the bottlenecks are, and how changes affect the numbers. The platform-first architecture’s performance advantages aren’t theoretical claims; they’re measurable in tools the field has refined for over a decade.

The next chapter (Chapter 68) covers real-time, collaboration, and multiplayer state — WebSockets, WebRTC, CRDTs, the architectural patterns for applications where multiple users interact with the same data simultaneously. Performance considerations carry forward (real-time work has its own performance profile), but the chapter’s focus is on the application-architecture patterns.

Take a Kit application — the one you’ve been building through Part IV’s and Part V’s exercises, or any application using the architecture. Run a comprehensive performance audit.

Lab measurements:

  1. Run Lighthouse against the production URL (or localhost with a production build). Record the Performance score and the Core Web Vitals.
  2. Run the Chrome DevTools Performance panel. Capture a trace of an initial page load. Note the LCP element and timing.
  3. Bundle analysis. Use source-map-explorer or your bundler’s stats tool to break down what’s in the JavaScript bundle.

RUM setup:

  1. Add the web-vitals library to the application as a module that subscribes to performance events.
  2. Forward the metrics to your analytics provider (or just log them to the console for the exercise).
  3. Load the application in multiple browsers, on multiple devices, over multiple networks. Compare the metrics.

Compare against a control:

  1. Find a similar application built on React + a meta-framework. Run Lighthouse against it.
  2. Compare the bundle sizes, the Core Web Vitals, the TBT.
  3. The Kit version should be measurably better on most metrics — by how much?

Test the no-JavaScript path:

  1. Disable JavaScript in dev tools.
  2. Verify the page is functional.
  3. Run Lighthouse with JavaScript disabled. The Performance score (with no JavaScript) should be very high, because the application is functional without JavaScript.

Reflect on:

  1. Which metrics are best in the Kit application? (Probably LCP, TBT, INP — the bundle-size-dependent ones.)
  2. Which metrics could be improved? (Possibly: LCP if the server response is slow, CLS if the application has image loading without size hints, INP if a specific module’s handler is slow.)
  3. What would happen if the application added a 500 KB framework dependency? (LCP slows, TBT increases, the budget breaks.)
  4. What’s the marginal cost of each new Kit module? (Small — modules are small, and they don’t block initial load.)

The performance work the architecture rewards is incremental and durable. Every kilobyte saved compounds across every page load for every user. The platform-first approach has the performance story going for it by default; the architecture’s role is to not give that advantage back through careless additions.