Why Performance Is a Moving Target
Performance work is no longer a final polish applied before launch. The days of relying on minification, file concatenation, and a few server tweaks are gone. Today, performance touches every layer of the stack, and the real difficulty is knowing where to focus effort. Is the bottleneck expensive JavaScript, slow font loading, heavy images, or rendering? Have you already exploited code-splitting, tree-shaking, progressive hydration, and modern protocols? Most importantly, where do you begin, and how do you make performance a lasting habit rather than a one-off fix?
The problem is compounded by variability. The same page can behave very differently depending on the user's device, browser, network type, latency, and the chain of infrastructure between them — CDNs, ISPs, caches, proxies, firewalls, load balancers, and servers all shape the final experience. That makes performance a metric that must be measured and monitored continually, not assumed.
The checklist below is an updated, practical overview of what matters for fast experiences on the web in 2021 — from metrics and tooling to front-end techniques — covering the full project lifecycle from first planning to final release.
Define the Metrics That Matter
Before optimizing, you need to know what you are optimizing for. User-centric performance is best captured by a small set of measurable milestones:
- First Contentful Paint (FCP) — when the first text or image is painted.
- Largest Contentful Paint (LCP) — when the main content has rendered.
- Time to Interactive (TTI) — when the page is reliably responsive.
- Total Blocking Time (TBT) — the time during which input responsiveness is blocked.
- Cumulative Layout Shift (CLS) — visual stability of the page as it loads.
- First Input Delay (FID) — the delay between a user's first interaction and the browser's response.
Practical, tooling-level metrics such as DOM complete time, time to first byte, and client-side CPU and memory usage also matter for ongoing monitoring. A good performance budget should tie back to these indicators so progress is visible and regressions are caught early.
Because performance data varies so widely, aim for the 75th percentile of real-user measurements, and look at both lab and field data. Lab data helps you debug; field data tells you what your users actually experience.
Setting the Stage: Planning and Objectives
Before diving into micro-optimizations, it is essential to establish definitive, measurable targets. These goals will shape all subsequent decisions. While the approaches below are opinionated, the key is to set your priorities early in the process.
Fostering a Performance Culture
A common bottleneck isn't technical know-how, but organizational alignment. Developers often know the fixes, but without buy-in from business stakeholders, performance efforts become fragmented. To secure that backing, build a case study or proof of concept that links speed—especially Core Web Vitals—to the KPIs that matter to the business. For instance, showing a correlation between conversion rates and load times, or demonstrating how speed impacts search bot crawl rates, makes the value tangible.
Guide the conversation by listening to common complaints from customer service and sales teams, and study analytics for high bounce rates and conversion drops. Tailor the argument to the specific group of stakeholders you are addressing. Run performance experiments and measure the outcomes on both mobile and desktop using tools like Google Analytics. Data from published case studies on sites like WPO Stats can also help increase business sensitivity to performance's impact.
For those looking to implement a long-term strategy, Allison McKnight’s talk, “Building Performance for the Long Term,” provides a comprehensive case study from Etsy, while Tammy Everts has spoken on the habits of effective performance teams. Keep in mind that web performance is a distribution, not a single number. As Karolina Szczur noted, expecting one metric to define success is flawed, so your goals should be granular, trackable, and tangible.
How Fast is Fast Enough?
Psychological research suggests that to make users *feel* your site is faster than a competitor's, it must be at least 20% faster. Study your main competitors, gather performance metrics on mobile and desktop, and set thresholds to beat them. Ensure you have a thorough picture of your own users' experiences by studying your analytics first; you can then test against the 90th percentile's experience.
To start, you can use the Chrome UX Report (CrUX), a ready-made RUM dataset, for a Chrome-specific view of competitor performance. Tools like Treo, powered by CrUX, also provide useful performance distributions, including Core Web Vitals. Note that new CrUX datasets are released on the second Tuesday of each month. Other options include:
- Addy Osmani’s Chrome UX Report Compare Tool.
- Speed Scorecard, which also includes a revenue impact estimator.
- SiteSpeed CI, a tool based on synthetic testing.
For deeper analysis, you can combine CrUX data with your own to pinpoint slowdowns. Harry Roberts uses a Site-Speed Topography Spreadsheet to break down performance by key page types, which can be customized to your needs. If you want to go all in, you can run a Lighthouse audit on every page of a site using Lighthouse Parade, saving the output as a CSV.
Collect this data, set up a spreadsheet, and shave off that 20% to formulate your performance budgets. You now have something concrete to test against. To get started, review Addy Osmani's guide on performance budgeting, Lara Hogan's tips for designers approaching new projects, or Harry Roberts' method for auditing third-party impact. Tools like the Performance Budget Calculator and perf-budget-calculator can also aid in creating your budgets. It's often useful to make both the budget and current performance visible with dashboards from SiteSpeed.io, SpeedCurve, or Calibre.
Once defined, integrate budgets into your build process using tools like Webpack Performance Hints, Lighthouse CI, PWMetrics, or Sitespeed CI to enforce them on pull requests. You can also integrate budgets into Lighthouse via Lightwallet, or use LHCI Action for easy GitHub Actions integration.
Performance awareness shouldn't stop at budgets. Consider creating a custom ESLint rule to prohibit importing from known dependency-heavy files, as Pinterest did. Also, during the design process, plan a loading sequence and prioritize which parts are critical. This will help define the order of CSS and JavaScript imports and clarify what can be deferred. Once you’ve established a culture of performance, aim to be 20% faster than your former self to keep priorities aligned as time goes on.
Choosing Your Metrics
Not all metrics are created equal. The most relevant ones for your application are typically determined by how fast you can render the most important pixels and how quickly the interface responds to input. The perception of snappiness matters more than a full page load event. Prioritize metrics that reflect your customers' experience, as traditional metrics like onLoad and DOMContentLoaded don't tell the whole story.
- Quantity-based metrics (request count, weight) are good for monitoring changes but do not explain user experience.
- Milestone metrics (Time To First Byte, Time To Interactive) describe the user experience but leave gaps in between.
- Rendering metrics (Start Render time, Speed Index) are useful for tweaking rendering performance but miss the arrival of important content.
- Custom metrics measure specific user events, like Twitter’s Time To First Tweet, but are hard to scale for comparison.
In most cases, a specific set of metrics provides the most insight:
- Time to Interactive (TTI): The point where layout is stable and the main thread can handle user input. It is key for understanding user wait times.
- First Input Delay (FID): The time from a user's first interaction to the browser's ability to respond. This metric should only be used in real user monitoring (RUM).
- Largest Contentful Paint (LCP): The point when the page's most important content has likely loaded. It assumes the largest element in the viewport is the most important.
- Total Blocking Time (TBT): The total time between the first paint and TTI where the main thread was blocked for long enough to prevent input responsiveness. It measures the severity of a page's non-interactivity.
- Cumulative Layout Shift (CLS): A metric that tracks how often users experience unexpected layout shifts or reflows. A lower score is better.
- Speed Index: Measures how quickly page contents visually populate. While it is sensitive to viewport size, its importance is waning as LCP becomes more relevant.
- CPU Time Spent: Indicates how often and for how long the main thread is blocked by painting, rendering, and scripting. High CPU time signals a janky experience. WebPageTest can expose this via its "Capture Dev Tools Timeline" option.
- Component-Level CPU Costs: Stoyan Stefanov's proposal to understand the isolated CPU impact of JavaScript components using Puppeteer and Chrome.
- FrustrationIndex: Tim Vereecke's metric examines the gaps between key milestones to calculate a frustration score, like the time between the first visible content and when the page looks ready.
- Ad Weight Impact: For revenue-dependent sites, track the impact of ad-related code. Scripts can generate a video comparison via WebPageTest and report the delta.
- Deviation Metrics: Tracking the variance in your results can inform you of the reliability of your instruments and the impact of third-party scripts.
- Custom Metrics: Defined by your business needs, these require identifying important pixels and critical assets. Monitor Hero Rendering Times or use the Performance API to mark business-critical timestamps.
It is worth noting that First Meaningful Paint (FMP) has been deprecated due to inaccuracy in about 20% of cases and replaced with LCP. Also, be aware that FID and TTI do not account for scrolling, as it happens off the main thread; these metrics may be less important for content-heavy sites.
Prioritizing Core Web Vitals
Announced in May 2020, Core Web Vitals are a set of user-focused metrics representing different facets of the user experience. To pass the assessment, at least 75% of all page views should meet the "Good" range. These metrics gained traction quickly, especially as they became ranking signals for Google Search in May 2021. Here is a breakdown of each:
- Largest Contentful Paint (LCP) < 2.5 sec: This measures loading performance, reporting the render time of the largest image or text block in the viewport. LCP is affected by slow server responses, blocking CSS, JavaScript, font loading, and rendering work. Images are often the main culprit. With an LCP target of 2.5s on 3G, the maximum theoretical image size is only about 144KB. To see what is considered an LCP element, hover over the badge under "Timings" in the DevTools Performance Panel.
- First Input Delay (FID) < 100ms: This measures how long the browser was busy before it could react to user input, capturing delays from main thread tasks during page load. The goal is to stay within 50–100ms for interactions. Break up long tasks, code-split bundles, and minimize main thread work to achieve a good score.
- Cumulative Layout Shift (CLS) < 0.1: This measures visual stability, attributing a score for every unexpected shift of a visible element. Late-ad loading, image dimension changes, or late CSS can all negatively impact CLS.
These metrics are designed to evolve with a predictable annual cycle. Expect updates, such as promoting First Contentful Paint and revising FID thresholds. To stay ahead, some useful resources include the Web Vitals Leaderboard for competitor comparison, Core SERP Vitals for viewing metrics in search results, and tools to analyze or visualize CLS. Libraries like the web-vitals library can collect and send these metrics to your analytics platform.
While Core Web Vitals are exposed in most RUM solutions and are a strong starting point, they are not perfect. As Katie Sylor-Miller explains, problems include a lack of cross-browser support and difficulty correlating changes in FID and CLS with business outcomes. It’s best to combine them with custom-tailored metrics for a complete performance picture.
Testing Strategically
Accurate data collection requires thoughtful device and network conditions. Your analytics may not show the full picture, as users with slow devices often abandon a site before returning. A representative test device is typically an Android device that is at least 24 months old and costs $200 or less, running on slow 3G (400ms RTT, 400kbps transfer). Look to current bestseller lists for a target market approximation.
Suitable devices include an older Moto G4/G5 Plus, a mid-range Samsung, or a low-end Rockchip or Mediatek device. Watch the chipsets to avoid over-representing a single type. If no physical device is available, emulate mobile conditions on desktop by throttling the network to 3G (e.g., 300ms RTT, 1.6 Mbps down) and slowing the CPU 5×. Expect a 4×–5× slowdown on mobile devices compared to desktops.
Balancing Lab and Field Data
A robust performance strategy should use both lab and field data. Synthetic testing tools (Lighthouse, Calibre, WebPageTest) gather reproducible data in a lab environment, which is useful during development to identify and fix issues. In contrast, Real User Monitoring (RUM) tools (SpeedCurve, New Relic) collect field data from actual user interactions, which is essential for long-term maintenance and understanding live bottlenecks. By tapping into built-in APIs like Navigation Timing and Resource Timing, you can see the full lifecycle of your application. For network testing, always prefer network-level throttlers external to the browser, as DevTools can have issues interacting with HTTP/2 push.
When using Lighthouse, you can further integrate it into your workflow:
- Use Lighthouse CI to track scores over time.
- Run Lighthouse in GitHub Actions for a report on every pull request.
- Run a mass audit via Lighthouse Parade and save results to a CSV.
- Lighthouse is also available for Firefox, though it reports based on a headless Chrome user-agent.
Finally, be sure to set up "clean" and "customer" profiles for testing. While clean profiles with no extensions are standard, some common browser extensions have a profound performance impact. Testing with a "customer" profile that includes popular extensions can give you a realistic view of the experience for a significant portion of your users.
Make sure performance goals are known across the team, as every design and marketing decision has performance implications. Distributing ownership and mapping these decisions against the established budget will streamline future optimization work.
Performance Targets Worth Setting
Setting concrete, measurable performance budgets early is essential. Without clear numbers, it is too easy to let regressions slide. The widely cited RAIL model provides a useful framework for thinking about user-centered performance. It sets two primary targets: interactions should respond in under 100 milliseconds, and animations should run at 60 frames per second. For the former, the main thread must yield control at least every 50 milliseconds so input events can be processed in time. The Estimated Input Latency metric in Lighthouse reports whether you are meeting this threshold, and it should ideally stay below 50ms.
For smooth animation, each frame must be produced in under 16.6 milliseconds (1 second divided by 60). In practice, you should aim for closer to 10 milliseconds to leave the browser time to paint the frame before the deadline. Screens running at 120Hz, such as the iPad Pro, are beginning to push these limits further, though 120fps is not yet a realistic target for most projects.
It also pays to be pessimistic about performance expectations but optimistic in interface design. Use browser idle time for non-critical work, as demonstrated by libraries like idlize, idle-until-urgent, and react-idle. These runtime targets are distinct from loading performance; they concern how snappy the interface feels once the page is present.
Loading Budgets on Real Hardware
For loading performance, a reasonable set of goals starts with a First Input Delay under 100ms (ideally 70ms or less), a Largest Contentful Paint under 2.5 seconds, and a Time to Interactive under 5 seconds on a slow 3G connection. For repeat visits, a sub-2-second TTI is a good stretch goal, but it typically requires a service worker. A useful baseline device is a low-cost Android phone like the Moto G4, on an emulated network with 400ms round-trip time and 400kbps transfer speed.
Two major constraints define what is achievable. The first is network delivery. Because of TCP Slow Start, the first round trip can carry only roughly 14KB of data — about 10 TCP packets of 1460 bytes each. This is the only payload that can reach the user within the first second at 400ms RTT, considering mobile wake-up times. This makes the initial HTML the most critical chunk of the entire payload.
TCP generally underutilizes the available bandwidth. Google’s TCP Bottleneck Bandwidth and RTT (BBR) algorithm addresses this by responding to actual congestion rather than packet loss, resulting in higher throughput and lower latency. BBR is now available on platforms like Google Cloud and Amazon CloudFront.
The second constraint is hardware. JavaScript parsing and execution on modest mobile CPUs are the main bottleneck. A budget of 170KB of gzipped JavaScript can take up to 1 second to parse and compile on a mid-range phone. Decompressed, that bundle expands to roughly 0.7MB, which can already degrade the experience on a device like the Moto G4 or G5 Plus.
These budgets are not static. On slower connections, every byte is more expensive regardless of how it is used. A fixed budget can therefore misrepresent the real cost to users. It is better to think of performance budgets as adaptive, scaling with the network conditions and device capabilities you are targeting.
Real-World Numbers
For emerging markets — including South East Asia, Africa, and India — the constraints are even harsher. Low-cost feature phones, expensive mobile data, and spotty network coverage demand a much tighter budget. The PRPL-30 pattern is one framework designed for these environments, focusing on a 130-170KB gzipped budget as a reasonable upper boundary.
Most sites are far from this goal. The median JavaScript bundle today sits around 452KB, an increase of over 50% since early 2015. On a mid-range mobile device, that translates to 12-20 seconds of Time-to-Interactive, an unacceptable experience for most users. Notably, if your site is using service workers and caching effectively, users with repeat visits should see significantly better numbers.
Budgets based solely on bundle size are not the only way to define constraints. You can also set budgets around the browser’s main thread activity — for example, the time to first paint or the CPU impact of long tasks. Tools like Calibre, SpeedCurve, and Bundlesize can track these budgets and integrate them into your build pipeline to catch regressions automatically.
It may seem counterintuitive to set such rigid limits in an era of HTTP/2, accelerated mobile pages, and fast or affordable connections. Yet the unpredictability of real-world networks and devices — congested tower backhaul, data caps, proxy browsers, save-data modes, and roaming charges — makes these constraints just as relevant today as they were a decade ago.
Environment And Rendering Strategy
Build Tools And Code Splitting
Your build toolchain choice matters less than your ability to maintain it. Grunt, Gulp, Webpack, Parcel — if it produces the results you need without maintenance pain, it's the right tool. That said, Webpack remains the most established option with hundreds of optimization plugins available. Rollup and Snowpack continue to gain ground, and the Webpack Roadmap 2021 is worth watching.
A notable recent strategy is granular chunking with Webpack in Next.js and Gatsby. Rather than serving modules that aren't shared across every entry point to routes that don't use them, a server-side build manifest file determines which outputted chunks each entry point actually needs. SplitChunksPlugin creates multiple split chunks based on conditions that prevent fetching duplicated code across routes, improving page load time and caching during navigation. This shipped in Next.js 9.2 and Gatsby v2.20.7.
Webpack's learning curve is steep, but there are solid resources: the official documentation, Webpack — The Confusing Bits by Raja Rao, and An Annotated Webpack Config by Andrew Welch. Sean Larkin's free Webpack: The Core Concepts and Jeffrey Way's Webpack for everyone on Laracasts are good introductions. Webpack Fundamentals is a comprehensive four-hour FrontendMasters course with Larkin. The Webpack examples directory has hundreds of ready-to-use configurations, and there's a Webpack config configurator for generating a basic setup. awesome-webpack curates articles, videos, and examples for Angular, React, and framework-agnostic projects.
Etsy's case study on moving from RequireJS to Webpack shows production builds managing over 13,200 assets in 4 minutes on average. Ivan Akulov maintains both a Webpack performance tips Twitter thread and the awesome-webpack-perf GitHub repo with performance-focused plugins and tools.
Progressive Enhancement As Default
Keeping progressive enhancement as the guiding principle remains a safe bet. Design and build the core experience first, then enhance for capable browsers. If your site runs fast on a slow machine with a poor screen and sub-optimal network, it will only run faster on better hardware.
Adaptive module serving takes this further: "lite" core experiences for low-end devices, richer features for high-end ones. This approach isn't going away.
Set A Realistic Performance Baseline
JavaScript carries the heaviest cost of the experience — more than web fonts blocking rendering or images consuming memory. Performance bottlenecks have moved from server to client, forcing developers to account for network latency, thermal throttling, cache eviction, third-party scripts, disk I/O, IPC latency, extensions, antivirus, firewalls, background CPU tasks, and L2/L3 caching differences. Browsers have dramatically improved parse and compile speeds, but script execution remains the bottleneck.
Tim Kadlec's research on framework costs highlights that in practice the issue isn't just a single framework — it's often multiple frameworks in use: an aging jQuery being migrated to something modern, plus legacy Angular apps. The cumulative cost of JavaScript bytes and CPU execution can make experiences barely usable on high-end devices, much less phones. Modern frameworks generally aren't prioritizing less powerful devices, and sites with React or Angular tend to spend more CPU time than others. As Kadlec puts it: "if you're using a framework to build your site, you're making a trade-off in terms of initial performance — even in the best of scenarios."
Framework Costs And Choices
Do You Really Need That Framework?
Not every project needs a framework, and Netflix demonstrated that not every page of a single-page application needs one either. Removing React, several libraries, and corresponding app code from the client reduced JavaScript by over 200KB and cut Time-to-Interactivity by more than 50% on the logged-out homepage. Netflix then used the time on the landing page to prefetch React for likely next pages.
You can remove framework JavaScript from critical pages entirely. gatsby-plugin-no-javascript strips all Gatsby-generated JavaScript from static HTML files. Vercel also offers experimental support for disabling runtime JavaScript in production on selected pages.
Framework choices stick for years, so they must be informed and performance-aware. The default cost is substantial: 58.6% of React pages ship over 1 MB of JavaScript, and only 36% of Vue.js page loads hit a First Contentful Paint under 1.5 seconds. Ankur Sethi's study of baseline framework costs found React apps won't load faster than ~1.1 seconds on an average phone in India no matter how optimized, Angular takes at least 2.7 seconds to boot, and Vue users wait at least 1 second. SPA's can be made fast, but they're not fast out of the box.
Lightweight options — Preact, Inferno, Vue, Svelte, Alpine, or Polymer — may do the job with a smaller baseline. Seb Markbåge suggests a useful measurement: render a view, delete it, render it again. The first render warms up lazily compiled code that scales well; the second emulates code reuse on a growing page. Sacha Greif's 12-point scoring system helps evaluate libraries on features, accessibility, stability, performance, package ecosystem, community, learning curve, documentation, tooling, track record, team, compatibility, and security.
For long-term data, Perf Track shows origin-aggregated Core Web Vitals for sites built with Angular, React, Vue, Polymer, Preact, Ember, Svelte, and AMP, with breakdowns by Gatsby, Next.js, Create React App, Nuxt.js, and Sapper. Default stacks that perform reasonably out of the box include Gatsby (React), Next.js (React), Vuepress (Vue), Preact CLI, and PWA Starter Kit. Also see web.dev's framework-specific performance guidance for React and Angular.
If you want to rethink SPAs entirely, Turbolinks is a 15KB library that renders views using HTML instead of JSON. When you follow a link, it fetches the page, swaps the <body>, and merges the <head>, avoiding full page loads. See the announcement and Hotwire documentation for the full stack.
Rendering Approaches: Choose All Of Them
Aim for progressive booting: use server-side rendering for a quick First Contentful Paint, while keeping Time to Interactive close. Late JavaScript parsing and execution can lock the main thread, turning interactivity into a sluggish mess. Break up function execution into separate async tasks, favor requestIdleCallback, and use WebPack's dynamic import() support to defer parse and compile costs until needed.
Time to Interactive measures the time before a five-second window passes where no JavaScript task exceeds 50ms. If a task exceeds that threshold, the window restarts. The browser may flip between Interactive and Frozen states as tasks process.
Paul Lewis notes most frameworks lack a concept of priority developers can surface, making progressive booting awkward. Options are emerging, detailed in Houssein Djirdeh and Jason Miller's talk on Rendering on the Web and Modern Front-End Architectures:
- Full Server-Side Rendering (SSR). All requests handled on the server, returning finished HTML that browsers render immediately. The FCP-TTI gap is small, but server think time increases Time To First Byte, and you lose responsive features. No DOM APIs available.
- Static Rendering. Build the SPA and prerender every possible URL to static HTML at build time. HTML isn't generated on the fly, so TTFB stays consistently fast. Netflix adopted this approach, reducing loading and TTI by 50%.
- SSR With (Re)Hydration (Universal Rendering). The server returns HTML plus a script booting a client-side app. The goal is fast FCP with continued rendering via
ReactDOMServer'srenderToString, Vue'svue-server-renderer, Angular's@nguniversal, or built-in support in Next.js and Nuxt.js. In practice, rehydration is expensive, creating a large FCP-TTI gap and increased First Input Delay. - Streaming SSR With Progressive Hydration. Render multiple requests concurrently, streaming content chunks to improve TTFB. React uses
renderToNodeStream(); Vue hasrenderToStream(); React Suspense offers asynchronous rendering. Client-side, break the app into standalone scripts via code splitting and hydrate progressively — critical components first, others when in view, needed for interaction, or when idle. Markus Oberlehner wrote on reducing TTI of SSR Vue apps through interaction-based hydration and built vue-lazy-hydration. Angular is working on progressive hydration with Ivy Universal. We've also seen partial hydration with Preact and Next.js. - Trisomorphic Rendering. With a service worker installed, streaming server rendering handles initial navigations, then the service worker prerenders HTML for subsequent ones, enabling SPA-style in-session navigation. Works when server, client, and service worker share templating and routing code.
- CSR With Prerendering. Render the application to static HTML at build time — the initial state of a client-side app — but the client must boot to become interactive. Gatsby's static builds use
renderToStaticMarkupinstead ofrenderToString, preloading the main JS chunk and prefetching future routes without unnecessary DOM attributes. Vuepress does this for Vue; prerender-loader works with Webpack; Navi supports static rendering. Good TTFB and FCP, but content can't change much and URLs must be known ahead of time. - Full Client-Side Rendering (CSR). All logic, rendering, and booting happen client-side, typically creating a huge FCP-TTI gap and sluggish interactions. Aggressive code-splitting becomes essential. For pages needing little interactivity, SSR usually wins. If SSR isn't possible, try The App Shell Model. In general, SSR is faster than CSR.
For advanced apps, strict client-side or server-side approaches both fail if done poorly. Limit full client-side frameworks to pages that genuinely require them. Render important pixels as soon as two possible, shrink the gap between first render and TTI, prerender when content is static, stream HTML in chunks with SSR, and apply progressive hydration — on visibility, interaction, or during idle time.
Static Generation And API Design
Consider what can serve statically from a CDN. Large e-commerce sites with thousands of products and deep personalization may still benefit from statically served landing pages decoupled from a framework. Static site generators abound and produce fast pages that scale well — the more you pre-build, the better.
Markus Oberlehner's guide to partially hydrated static websites with Eleventy and Preact shows partial and lazy hydration, a client entry file, Babel for Preact, and bundling with Rollup. Build time becomes a real constraint at scale. Gatsby's incremental builds improve build times by 60 times, integrating with WordPress, Contentful, Drupal, Netlify CMS, and others.
Next.js has ahead-of-time and incremental static generation, adding new pages at runtime and updating existing ones by re-rendering in the background as traffic comes in. For a leaner stack, Nicola Goutay's talk on Eleventy, Alpine, and Tailwind compares CSR, SSR, and middle grounds on a working repo.
The PRPL pattern and application shell architecture still apply: push minimal code for the initial route, cache and precache with a service worker, and lazy-load everything else asynchronously.
API Performance Matters
APIs mediate between your app and data via endpoints. REST remains a reliable, scalable choice, but any server delay propagates to rendering. Components pulling from multiple endpoints — an article plus author photos per comment — need several round-trips. GraphQL requests barely more than one response: everything the component needs, no more. Because responses follow your schema, data arrives pre-organized, and you can strip state-management code that slows the client.
If GraphQL is new or causing pain, dig into Eric Baer's two-part GraphQL primer, Leonardo Losoviz' server-optimization guide, and Wojciech Trocki's explainers of GraphQL performance.
AMP, Instant Articles, And CDNs
Google's AMP, Facebook's Instant Articles, and Apple News promise guaranteed performance and platform discoverability. But Tim Kadlec's research concludes AMP documents aren't necessarily faster than counterparts — "AMP is not what makes the biggest difference from a performance perspective." With AMP no longer required for Top Stories, some publishers are moving back to standard stacks. You can still build progressive web AMPs, though maintaining a separate version of your content in walled gardens without real URLs is its own cost.
Choose your CDN for more than static assets. It handles compression, image optimization, service worker support, A/B testing, and edge-side includes. Confirm HTTP/3 support. Read Katie Hempenius's CDN guide — good advice on choice, configuration, and enabling Brotli, TLS 1.3, HTTP/2, and HTTP/3.
Be cautious: research by Patrick Meenan and Andy Davies shows HTTP/2 prioritization is broken on many CDNs, detailed in Meenan's talk. Comparison sites like CDN Comparison, CDN Perf (300M daily tests based on real user data), CDN Planet Guides, and the HTTP Archive's CDN chapter provide data on provider features, RTT, TLS management and negotiation time.
Asset Optimization: Compression, Images, and Fonts
Compression: Brotli Is Worth The Switch
Introduced by Google in 2015, Brotli is an open-source lossless data format now supported in all modern browsers. When you enable it, you can often see single-digit improvements on top of Gzip, and in some cases, double-digit size reductions that can meaningfully improve your FCP timing.
Brotli's compression ratio comes at a CPU cost. Higher quality levels demand more processing power and can slow down dynamic, on-the-fly compression. However, it is important to note that Brotli at compression level 4 is both smaller and compresses faster than Gzip. For pre-compressed static assets, you can afford to use the slowest, highest levels of compression.
To get around dynamic compression costs, the format includes a built-in static dictionary. Research by Felix Hanau shows that by using a more specialized subset of that dictionary based on the file's Content-Type, you can improve compression at levels 5 through 9 with a "negligible performance impact (1% to 3% more CPU compared to 12% normally)."
A practical strategy is to pre-compress static assets with Brotli and Gzip at the highest level, and compress dynamic HTML on the fly with Brotli at level 4-6, ensuring your server handles content negotiation properly. As of early 2021, roughly 60% of HTTP responses on the web are still delivered without any text-based compression, often leaving this an easy win with a simple configuration change.
Brotli also opens a path to efficient recompression. Research by Elena Kirilenko demonstrates that when dynamically compressing assets that resemble content available ahead of time (like JavaScript bundle subsets or HTML built from known templates), you can achieve a 5.3% improvement in compression and 39% improvement in speed when removing just 10% of content. Brotli is fine for any plaintext payload, including HTML, CSS, SVG, JavaScript, and JSON, though it only works over HTTPS.
Adaptive Images and Client Hints
Responsive images with srcset, sizes, and the <picture> element are baseline practice. For sites with serious media footprints, go a step further with adaptive media loading: is as serving light experiences to users on slow networks when possible. In React contexts, react-adaptive-hooks and client hints on the server can deliver this behavior.
Client hints—HTTP request headers such as DPR, Viewport-Width, and Save-Data—tell the server about the user's browser and screen so it can make smarter image decisions. Client hints don't replace responsive images: the <picture> element provides art direction control, Client hints automate resource selection, and Service Workers give complete request response management. A service worker can work with client hints to, for instance, rewrite URLs to point at a CDN.
Real-world testing shows client hints can generate meaningful byte savings; one test measured 42% savings on images. They are well-supported across Chromium-based browsers but remain under consideration in Firefox. A normal responsive images markup still works in browsers that ignore Client Hints, which ensures your fallback is as strong as your optimization path.
Background Images and Image Compression
Background images deserve a responsive treatment as well. Use image-set (now supported in Safari 14 and most modern browsers except Firefox) to conditionally serve low-resolution versions for 1x displays and high-resolution versions for 2x, or a 600dpi image for print.
Serving progressive JPEGs with low-quality placeholders gets a usable experience up fast. With WebP, you may reduce overall payload more, but you won't have progressive rendering; instead, users might see a half-empty image for a noticeable stretch of time. Decide between payload size (WebP) and perceived speed (JPEG) depending on that tradeoff’s role in your project.
WebP is now universally supported since Safari 14, making it a low-risk add to your image pipeline, and conversions yield both meaningful syntactically smaller images and flexible support with <picture>, Accept headers, and CDNs. Standard tooling (cwebp, libwebp) plus WordPress and Joomla extensions bring the format close to click-and-run. For videos and still images alike, WebP lends itself to modern implementations down to Sketch and Photoshop plugins.
Adopting AVIF
AVIF has arrived, derived from AV1 video keyframes. It's open and royalty-free, supports lossy and lossless compression, animation, and lossy alpha, and holds up well with sharp lines and solid colors. Across tests, AVIF produces median file size savings as high as 50% at comparable similarity metrics to JPEG and often outperforms WebP handily. When supporting image loading optimization, it yields clear wins.
[BLOCK_43]
While its best compression is CPU-intense during encoding, AVIF is fast to decode. Adoption has spread: it works in Chrome and Firefox with hope for Safari (which has involvement in AV1). The only missing big feature is progressive image decoding, so for some users, JPEG may win on perceived speed despite delivering bigger byte payloads.
For optimal serving across formats: with the <picture> element, send AVIF if supported, fall back to WebP, then JPEG or PNG. Keep current no-motion, reduce-motion patterns in mind when considering animation. AVIF compares favorably against GIF and WebP and is outperforming both across real-world animation benchmarks.
Tools exist for encoding (Squoosh, AVIF.io, libavif), testing with developer tools, and setting up AVIF support using PostCSS and cloud functions—one Cloudflare implementation practically delivers AVIF through workers that alter returned HTML and infers from the Accept header. A good check: AVIF can sometimes beat large SVGs too, although it remains no replacement for those vector formats.
Whether AVIF is the future is still open for debate: JPEG XL, another free and open format in its current stages, shows promising results. But standard web deployment of JPEG XL isn't yet possible.
Optimizing Legacy Formats
When you can't re-encode to AVIF, your next move is tightening JPEG, PNG, and SVG outputs:
- Progressive JPEGs: encode with mozJPEG (it improves start-render timing) or Guetzli for strong perceptual quality—but expect slow processing.
- Automate SVG cleaning with SVGO and SVGOMG. For Chrome-based previews, svg-grabber gets all assets to quick inspect or download.
- Always assign proper width and height (or
aspect-ratioin modern CSS) to establish the right layout slot and avoid content jumps. - Use lazy-loading, with native lazy-loading plus an IntersectionObserver library, for below-the-fold assets. Hybrid lazy loading stops unrequested carousel or accordion assets from downloading.
- Don't ignore background images: these also benefit from
srcsetpatterns and device-pixel ratio checks.
Beyond simple compression, a broad class of techniques improves perceived performance: loading an image directly with <img> hidden from view can preload it for background use; sizes swapping handles magnification UI and display differences across breakpoints. Even static optimization (reducing colors, cropping smaller, blurring parts—at 0% quality images aren't always unacceptable) offers entire avenues to edit bytes without end users noticing differences. Check both width/height and intrinsicsize for early layout reservation.
Cloud and CDN tooling pushes optimization further: image actions link into GitHub workflows so no uncompressed binary ever reaches production. Lepton from Dropbox losslessly strips JPEG weight by around 22% for internal storage. Responsive Image Breakpoints Generators or services like Cloudinary or Imgix automate the recurring resizing work and check the efficiency of your markup with dedicated CLI tools.
Rethinking Video Delivery
Animated GIFs are heavy. If you’re still using them, the better routes are to embed animated WebP with a GIF fallback or replace them with a looped HTML5 video—videos decode much faster than GIF and deliver excellent compression.
Choose AV1 for modern browsers (it's fast, free, and supported), and H.264 in MP4 containers for maximum reach; multipass encoding, blurring if appropriate, and stashing moov atom metadata at the file’s head all help. If you have large background videos on promotional landings, show the first frame as a still (or a short, heavily compressed segment) until more buffering frees up network.
Video stream quality strongly influences consumer behavior: if startup delay surpasses two seconds, each added second pushes about 5.8% more viewers to bail. Many video elements don't set preload at all (which forces needless network activity). Smart video tags will include preload="none" plus adaptive sources for less capable environments to respect bandwidth.
Web Font Delivery Tradeoffs
Start by asking whether a system font satisfies the design. If a custom face is necessary, cut file sizes with subsetting, WOFF2, and a solid loading strategy. Google Fonts, WordPress plugins, and popular frameworks all accommodate self-hosting—often landing in smaller payloads than CDNs but taking care to stay inside HTTP/2 performance limits.
Two-stage font rendering—a minimal set first, then the rest—remains a valuable method. Critical FOFT with preload best delivers that path using an initial small supersubset to render, and fills out families casually. Using local() to detect preinstalled names is problematic since widely distributed fonts can confuse your “local” with differently rendered versions; it’s best to never mix locally installed Font versions with web-downloaded ones.
font-display, at least the Google option, now enables tuning that layout breakdown on slow connections, and `&text` parameters can shave 90% directly from your font requests. Users with Data Saver Mode, slow connectivity—detected with the Network Information API—or prefers-reduced-data deserve graceful degradation paths as accessibility treats motion carefully. Metrics like All Text Visible track when content is ready in actual fonts.
Watch that resource hints work consistently with font requirements: preloading fonts might create network contention with earlier critical assets. If necessary, defer least-critical fonts until blocks following initial render pass. Variable fonts give a broader design space but come with large single serial requests; retaining subsets and reflows thinking applies there too.
Build Pipeline: Shipping Less, Smarter
Before touching a bundler, define what core, enhanced, and extra mean for your product. Create a spreadsheet inventory of every JavaScript file, image, font, and third-party widget. The core experience must be fully accessible for legacy browsers; enhancements can target capable browsers; extras such as web fonts, carousels, and social widgets deserve lazy-loading. Optimize in that priority order.
Use Modern JavaScript Delivery
The module/nomodule pattern (often called differential serving) compiles two distinct bundles: one transformed with Babel and polyfills for older browsers, and another modern, untransformed build for everything else. Because native script type="module" scripts are deferred by default, the browser can download the main module while HTML parsing continues.
This pattern carries risk — some clients request both bundles. Jeremy Wagner’s less risky differential serving pattern avoids that but bypasses the preload scanner, which has its own performance implications. Rollup supports modules as an output format, Parcel 2 has module support, and Webpack users can automate the process with module-nomodule-plugin.
Feature detection alone cannot judge device capability. A cheap Android phone running Chrome will cut the mustard despite limited RAM and CPU. The Device Memory Client Hints Header stays limited to Blink; where supported, you can feature-detect via the Device Memory JavaScript API and fall back to module/nomodule where it isn’t.
Trim the Bundle
Tree-shaking removes unused imports, and scope hoisting flattens import chains into single inlined functions. Code-splitting breaks the codebase into chunks loaded on demand, keeping initial downloads small. Track which chunks actually matter: use DevTools code coverage to find unused CSS and JavaScript, then lazy-load those modules with import().
Webpack’s optimization.splitChunks: 'all' automatically code-splits entry bundles for caching, and optimization.runtimeChunk: true isolates the Webpack runtime into its own chunk. Further plugins worth adding to the pipeline: purgecss-webpack-plugin for unused classes, workbox-webpack-plugin to generate a precaching service worker, speed-measure-webpack-plugin to identify build bottlenecks, and duplicate-package-checker-webpack-plugin to warn about multiple versions of the same package. A /*#__PURE__*/ comment before a function call that produces an unused result lets Uglify and Terser drop it.
/*#__PURE__*/. Via Ivan Akulov.(Large preview)Mark pure function calls with /*#__PURE__*/.
Move Work Off the Main Thread
When Time-to-Interactive degrades as the codebase grows, offload heavy work to Web Workers. DOM operations and JavaScript compete on the main thread; workers run on a separate thread where DOM access isn’t available. Common use cases include prefetching data and Progressive Web Apps. Starting with Chrome 80, module workers align script loading and execution with script type="module", including dynamic imports for lazy-loading.
Useful starting points:
workerizemoves a module into a Web Worker with async proxy exports;workerize-loaderandworker-pluginhandle the Webpack integration.- Comlink streamlines page-worker communication.
- Avoid worker code that needs the DOM — the code must live in a separate file.
Consider WebAssembly for Hot Paths
WebAssembly suits computationally intensive apps such as games; for most web applications JavaScript is still the better fit. Browser support is excellent, and calls between JavaScript and WASM have become fast enough to make the trade worthwhile. It also runs on Fastly’s edge cloud.
Serve Legacy Code Only to Legacy Browsers
ES2017 is well supported in modern browsers. Use babelEsmPlugin to transpile only the features those browsers lack. Modern browsers load the native module bundle via script type="module"; older browsers get the legacy build with nomodule. The <link rel="modulepreload"> header initiates earlier, higher-priority fetches of module scripts.
Clean Up Legacy and Dead Code
Long-lived projects accumulate dependencies that outlive their purpose. Approach legacy removal incrementally: GitHub’s jQuery removal shows how to measure the ratio of legacy calls over time, discourage use in pull requests via CI alerts, and transition with polyfills.
Chrome’s code coverage tool reveals which CSS and JavaScript actually execute. Automate collection with Puppeteer, repeat the coverage profile after lazy-loading detected unused modules, and validate the improvement. Test both modern and legacy browser profiles because they parse differently.
To find dead CSS without tooling, Harry Roberts suggests a 1×1px transparent GIF with a distinctive URL matched as a background on the suspect selector. Absence from server logs after months indicates the component never renders.
Additional tools: purgecss, UnCSS, and Helium remove unused CSS rules. Newer DevTools can record the tests themselves.
Shrink JavaScript Bundles
Teams routinely ship full libraries when only a fraction is needed, along with polyfills that never execute. The webpack-libs-optimizations repo automates removing unused methods and polyfills at build time. Use polyfill.io to return only the polyfills a requesting browser actually needs. Replace heavyweight libraries with lighter alternatives; the archived Moment.js project can be swapped for the native Intl API, Day.js, date-fns, or Luxon. One analysis found that switching from Moment.js to date-fns shaved roughly 300ms off first paint on low-end mobile hardware over 3G.
Audit bundles with Bundlephobia, size-limit, source-map-explorer, or webpack-bundle-analyzer. Svelte and the Rawact Babel plugin (which transpiles React components to native DOM operations) both offer ways to ship compiled output rather than a full framework.
Strategies for Hydration and SPAs
Partial hydration sends only the JavaScript pieces needed, hydrating isolated islands instead of the entire application after SSR. Welt.de documents notable performance gains from this approach. Alternatives include progressive hydration in React, lazy-hydration in Vue, and the Import on Interaction pattern for lazy-loading non-critical resources on first use.
For client-side framework applications, the best-case strategy stays conservative:
- Refactor stateful components into stateless ones, prerendering when possible.
- Replace simple interactivity on prerendered or server-only components with standalone event listeners.
- If client-side hydration is unavoidable, lazy-hydrate on visibility or interaction, scheduled within
requestIdleCallback.
Watch React Server Components, a zero-bundle-size component type in development that would eliminate their payload entirely.
Predictive Prefetching
Guess.js builds a machine-learning model from Google Analytics navigation patterns to prefetch JavaScript for the page a user is most likely to visit next. It integrates with Next.js, Angular, and React, or via a Webpack plugin. Prefetching has a data cost, so target only high-value pages such as checkout validation. Simpler link prefetchers include Quicklink, InstantClick, and Instant.page, all viewport-based; Quicklink also respects Data-Saver and avoids 2G networks.
Optimizing for the Target Engine
If V8 dominates your audience, script streaming parses async or defer scripts on a background thread once download begins — use script defer in the <head> to let the browser discover it early. V8’s code caching rewards separating library code from application code, avoiding inline scripts, and grouping small files.
General JavaScript engine habits worth keeping:
- Extend scroll performance by registering
passive: trueonscrollandtouch*listeners wherepreventDefault()isn’t needed. - Avoid re-exports — they hurt both loading and runtime performance.
- Use the
isInputPending()API to check for user input without yielding to the browser in long tasks. - Compress data with the CompressionStream API (Chrome 80+).
- Watch for detached window memory leaks; DevTools’
queryObjects()helps spot them.
Third Parties and Caching
Self-Host by Default
Public CDN caching no longer works as expected. Partitioned caching, implemented in Safari since 2013 and in Chrome since 2020, downloads the same third-party URL once per domain. Cache “sandboxing” for privacy means linking to the same library doesn’t improve arrival performance. First-party assets also stay cached longer than third-party ones, favor your own server for reliability and security.
Constraining Third-Party Scripts
Third-party code accounts for 57% of all JavaScript execution; the median mobile site talks to 12 third-party domains across roughly 37 requests. These often snowball into equally expensive fourth-party chains. Auditing matters because a single tag-manager script can spawn long tails of dependencies.
/* Before */
const App = () => {
return <div>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){...}
gtg('js', new Date());
</script>
</div>
}
/* After */
const App = () => {
const[isRendered, setRendered] = useState(false);
useEffect(() => setRendered(true));
return <div>
{isRendered ?
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){...}
gtg('js', new Date());
</script>
: null}
</div>
}
Deferring third-party execution only relieves parse blocking. Measure true impact by testing pages with and without scripts using WebPageTest. Request Map visualizes each third-party, its size, type, and trigger. Prefer a single self-hosted hostname, then identify lightweight fallbacks: a tracking pixel often replaces an entire tag.
Facades offer a stronger mitigation. A static mock resembling the embedded component loads only on interaction:
- lite-vimeo-embed or lite-vimeo for Vimeo
- lite-youtube-embed for YouTube
- react-live-chat-loader for live chat widgets
- lazyframe for arbitrary iframes
Anti-flicker snippets inject the worst client-side delays: default blocks render the page up to 4 seconds with opacity: 0 during A/B tests. Track how often the timeout actually fires, and prefer server-side A/B testing on CDN edge compute over client-side equivalents.
Contain what you cannot remove:
- Embed scripts in
<iframe>with a restrictivesandboxattribute — supported almost everywhere. - Race third-party resource downloads against a timeout in a service worker; return an empty response when they miss it.
- Fetch the vendor script from your own domains where possible.
- Apply a Content Security Policy that bans heavy feature classes.
- Use feature policies to opt in or out of browser capabilities (Blink support only).
Intersection Observer keeps ads functional from inside iframes without unconstrained DOM access. Before signing any vendor, check ThirdPartyWeb.Today which categorizes scripts by entity and typical execution cost. Replace sharing widgets with static buttons and interactive maps with static links where the UX permits.
Get HTTP Caching Right
Without explicit cache-control headers the browser defaults to heuristic caching based on last-modified, triggering both under- and over-caching. Static assets belong under a cache-forever policy: a year-long Cache-Control and Expires means no request occurs once cached. Version the URL when content changes.
API responses are the exception — use private, no store, not max-age=0, no-store:
Cache-Control: private, no-store
Add Cache-control: immutable for hashed CSS and JavaScript so browsers skip revalidation on reload:
Cache-Control: max-age: 31556952, immutable
That attribute cuts roughly half of 304 responses, though Chrome support remains unresolved. stale-while-revalidate defines an extra window after max-age where a stale asset serves instantly while the cache revalidates in the background. Chrome and Firefox support it since mid-2019, which eliminates the network round trip on repeat views.
Remove response headers that help no one, such as x-powered-by or X-XSS-Protection, and include the security set: Content-Security-Policy, X-Content-Type-Options. Remember that retrieving cache can cost hundreds of milliseconds, particularly with many cached objects on slower devices, and frequent CI/CD deployments invalidate caches more often than commonly assumed. Question the assumption that any cache hit is free.
Script Loading and Lazy Loading
How you deliver JavaScript to the browser has a direct impact on when rendering starts. The browser builds the DOM from HTML, the CSSOM from CSS, and then combines them into a rendering tree. Any JavaScript that must be resolved delays that process, so you have to explicitly tell the browser not to wait. The defer and async attributes are the mechanism for that.
In practice, defer is usually the better choice. Once an async script arrives, it executes immediately when ready — potentially blocking the HTML parser if it was already cached. With defer, scripts wait until HTML parsing is complete. And while multiple async files execute in non-deterministic order, defer preserves order. There are common misconceptions here: async doesn't mean "run whenever the script is ready" — it means it runs when the script is ready and all preceding synchronous work is done. If you place an async script after synchronous scripts, it's only as fast as the slowest synchronous script. Don't use both attributes together; modern browsers support both, but async always wins when both are present.
Lazy Loading: Beyond the Basics
For expensive components like heavy JavaScript, videos, iframes and widgets, lazy loading is generally recommended. Native lazy-loading via the loading attribute works for images and iframes in Chromium; the browser defers the resource until it reaches a calculated distance from the viewport. On 4G connections, experiments on Chrome for Android show that 97.5% of below-the-fold lazy-loaded images were fully loaded within 10ms of becoming visible.
For more granular control, the importance attribute (with high or low values) works on <script>, <img> and <link> elements in Blink — useful for deprioritizing carousel images. When you need finer control still, the Intersection Observer API provides asynchronous observation of when a target element intersects an ancestor or the top-level viewport. You create an IntersectionObserver with a callback and options, then add a target. The callback fires when the target becomes visible or invisible, and you can control exactly when through rootMargin and threshold.
This approach extends beyond images: performant scrollytelling, translation strings and even emoji can be lazy-loaded. Mobile Twitter achieved 80% faster JavaScript execution from its internationalization pipeline this way. But caution is warranted — lazy loading should be the exception, not the rule. Don't lazy-load anything users need quickly: product images, hero images or scripts required for main navigation interactivity.
Progressive Image Loading
Progressive image loading takes lazy loading further. Services like Facebook, Pinterest, Medium and Wolt load low-quality or blurry versions first, then swap in full-quality versions as the page continues loading. Techniques include BlurHash and LQIP (Low Quality Image Placeholders). Opinions differ on whether these improve user experience, but they do improve First Contentful Paint.
Tools like SQIP automate this by creating low-quality SVG placeholders; gradient-based CSS placeholders are another option. Since these placeholders are text, they compress well with standard text compression when embedded in HTML. Implement them with Intersection Observer, with a polyfill or immediate image loading as fallback for unsupported browsers. For a more sophisticated approach, trace images into primitive shapes and edges, load the lightweight SVG placeholder first, then transition to the bitmap.
Deferring Rendering Work
Complex layouts with many content blocks, images and videos make decoding and pixel rendering expensive, especially on low-end devices. content-visibility: auto lets the browser skip layout for children while a container is outside the viewport — say, for a footer or late sections on initial load.
Two caveats: content-visibility: auto behaves like overflow: hidden, though padding can work around that. And you risk introducing CLS when new content renders, so pair it with a properly sized contain-intrinsic-size placeholder. CSS Containment provides even more granular control, letting you manually skip layout, style and paint work for a DOM node's descendants when you only need size, alignment or computed styles.
For offscreen content you want available when needed without blocking the critical path, decoding="async" gives the browser permission to decode images off the main thread. Alternatively, display a placeholder, then trigger a network call via IntersectionObserver when the image approaches the viewport. Fade-in animations on render are a good companion practice.
Critical CSS
Collecting above-the-fold CSS and inlining it in the <head> reduces roundtrips and gets rendering started sooner. Keep critical CSS around 14KB — beyond that, additional roundtrips are needed. Tools like CriticalCSS and Critical generate critical CSS per template, though manual collection often works better. Inline critical CSS and lazy-load the rest, perhaps with the critters Webpack plugin. Filament Group's conditional inlining approach works well, as does converting inline code to static assets on the fly.
Loading full CSS asynchronously doesn't require libraries like loadCSS; the media="print" trick fetches CSS asynchronously while applying it to the screen once loaded.
For complex layouts, include layout groundwork in critical CSS to avoid massive recalculation and repainting that would hurt Core Web Vitals. Don't hide non-critical content while CSS loads, though — users on slow connections may never see it. Keeping content visible, even unstyled, is preferable.
Serving critical CSS as a separate file from the root domain can outperform inlining thanks to caching: Chrome speculatively opens a second HTTP connection to the root domain, avoiding a TCP connection for that CSS. HTTP/2 server push was once suggested for critical CSS delivery, but it had caching issues, race conditions and could bloat network buffers. Chrome plans to remove Server Push support.
Regrouping CSS and Streaming
Splitting the main CSS file into individual media queries lets the browser fetch critical CSS at high priority and everything else at low priority, off the critical path. Avoid placing <link rel="stylesheet" /> before async snippets. If scripts don't depend on stylesheets, put blocking scripts above blocking styles; if they do depend, split the JavaScript across the CSS.
For critical CSS caching, Scott Jehl's approach: add an ID to the style element, find it with JavaScript, store it in the Cache API as text/css, and set a cookie on the first visit so subsequent pages reference the cached asset externally instead of inlining.
Streaming responses let the page start working with the first chunk of data as soon as it arrives. Service workers can construct streams where the shell comes from cache but the body comes from the network — useful when a CMS server-renders HTML by stitching partial templates. Streaming the entire HTML response allows the browser's streaming HTML parser to work its magic; content inserted after page load misses that optimization. Support is partial in Chrome, Firefox, Safari and Edge, with streaming requests experimental in Chrome 85.
Connection- and Memory-Aware Components
Payload cost matters. The Save-Data client hint request header customizes applications for cost- and performance-constrained users. Options include rewriting high-DPI image requests to low-DPI, removing web fonts and parallax effects, disabling video autoplay, and downgrading image quality. The opt-in rate is substantial: 18% of global Android Chrome users have Lite Mode enabled, with higher rates on cheaper devices and significant outliers — over 34% in Canada versus ~7% in the US.
With Save-Data on, Chrome Mobile provides proxied experiences with deferred scripts, enforced font-display: swap and enforced lazy loading — but building your own experience beats relying on browser defaults. The Network Information API's navigator.connection.effectiveType uses RTT, downlink and effectiveType values to represent connection quality. A <Media /> component in React might render: offline placeholders with alt text on 2G/save-data; low-res images on 3G non-Retina; mid-res on 3G Retina; high-res Retina on 4G; HD video on the fastest connections. For videos, show the poster with a play icon on better connections; listen for canplaythrough and use Promise.race() to time out source loading in 2 seconds on poor connections.
Going further, the Device Memory API (navigator.deviceMemory) exposes RAM in gigabytes rounded down to the nearest power of two. Combined with hardware concurrency and network type, Umar Hansa shows how to defer expensive scripts via dynamic imports based on device capabilities.
Resource Hints and Preloading
Resource hints are the easiest performance win: dns-prefetch performs DNS lookups in the background, preconnect starts the connection handshake, prefetch requests resources for future use, and preload fetches resources without executing them. All are well-supported in modern browsers.
The old prerender hint had serious implementation issues — huge memory footprints, bandwidth waste, duplicated analytics hits. Chrome brought it back as NoState Prefetch: it fetches resources in advance but doesn't execute JavaScript or render anything, using only ~45MiB of memory and marking requests with Purpose: Prefetch. Portals are a newer effort toward privacy-conscious prerendering.
Use preload for resources clearly needed on the current page, prefetch for future navigations. At minimum use preconnect and dns-prefetch, ordered by priority since browsers limit parallel lookups. Preloading fonts is tricky — preload gets high importance and can leapfrog critical CSS. Note that preload accepts media, imagesrcset and imagesizes attributes, enabling responsive hero image preloading, and can preload JSON as fetch before JavaScript requests it. Preloaded assets land in the memory cache tied to the requesting page, but play well with HTTP cache — no network request if already there. The as attribute must be defined or nothing loads, and preloaded fonts without crossorigin will double fetch.
Service Workers
A local cache beats any network optimization. Service workers cache static assets and provide offline fallbacks. Phil Walton's approach sends smaller HTML payloads: the service worker requests minimal data (an HTML partial, Markdown, JSON), then programmatically transforms it into a full document. Once installed, users never request a full HTML page again.
Watch for Safari's range requests (Workbox has a module for this) and opaque responses from cross-origin resources — ensure proper CORS headers exist, avoid caching opaque responses unintentionally, and add the crossorigin attribute to <img> tags to opt into CORS mode. Common strategies include caching the app shell plus critical pages like offline and frontpage.
Service workers also run on CDN servers now, useful for A/B testing where HTML varies per user, or streaming HTML rewrites on the edge.
Rendering and Perceived Performance
Hitting 60 frames per second consistently beats variable frame rates. Use will-change to inform the browser about imminent changes. Debug repaints via DevTools: measure runtime rendering performance, enable Paint Flashing in Firefox, check "Highlight updates" and "Record why each component rendered" in React DevTools, or use Why Did You Render. Trigger only compositing via opacity and transform when possible — GPU-composited layer changes are the least expensive.
Perceived performance manages user psychology. Skeleton screens can keep users engaged — or test worst by some metrics, so test before deploying. Perception management, preemptive start, early completion and tolerance management all matter here.
Layout shifts are among the most disruptive experiences. Avoid inserting content above existing content unless responding to user interaction. Always set width and height on images so browsers reserve space. Use SVG or other placeholders for media and dynamic content. Prefer native lazy-loading (or hybrid approaches), group web font repaints, and transition from all fallback fonts to all web fonts at once, adjusting line-height and spacing with font-style-matcher. @font-face descriptors can override font metrics to emulate web fonts (enabled in Chrome 87).
Inline layout-critical CSS per template. Add overflow-y: scroll on html to display a scrollbar at first paint, preventing a 16px content shift — but note this breaks position: sticky. Reserve space for headers that become fixed on scroll. For tabs with varying content, CSS grid stacks keep containers at the height of the largest element. Infinite scroll and "Load more" cause shifts if content exists below the list — reserve space beforehand or remove bottom DOM elements. Measure all this with the Layout Instability API to calculate CLS scores in tests.
TLS, Certificates, And The Cost Of Trust
Every TLS handshake carries overhead that you can trim. One straightforward step is enabling OCSP stapling on your server. The Online Certificate Status Protocol (OCSP) lets a browser check whether an SSL certificate has been revoked without downloading a full Certificate Revocation List (CRL). When stapling is on, the server handles that check and attaches the timestamped, signed result to the handshake itself — removing a round trip and speeding up negotiation.
Your certificate choice also affects performance. Validation levels break down into three types:
- Domain Validation (DV) — confirms the requester owns the domain.
- Organisation Validation (OV) — confirms an organisation owns the domain.
- Extended Validation (EV) — confirms ownership with more rigorous human review.
All three share the same underlying technology; they differ only in included information and properties. EV certificates are pricier and slower to obtain because a human must review them, whereas DV certificates are often free — via Let’s Encrypt, for instance — and widely integrated into hosting providers and CDNs. As of this writing, Let’s Encrypt powers over 225 million sites.
The real problem is that EV certificates don’t fully support OCSP stapling. Without stapling, the client must contact the Certificate Authority itself during TLS negotiation. On poor connections, that extra request can cost 1000ms or more. From a performance standpoint, serving an OCSP-stapled DV certificate is the better play: cheaper to acquire, less hassle, and faster in practice. The situation may change once CRLite is broadly available.
Two final notes on certificates. First, with QUIC and HTTP/3 arriving, the TLS certificate chain is the main variable-sized element in the handshake — its size ranges from a few hundred bytes to over 10 KB. Large chains force multiple round trips, and compression matters because otherwise a chain might not fit in a single QUIC flight. Second, consider IPv6: with IPv4 space nearly exhausted and large mobile networks adopting IPv6 swiftly, dual-stack support lets both protocols run simultaneously. Studies show IPv6 can make sites 10-15% faster thanks to neighbor discovery and route optimization.
HTTP/2 And HTTP/3: Deployment Reality Checks
HTTP/2 has wide support and, for most sites, is a clear win — roughly 64% of all requests already run over it. It isn’t perfect; there are known prioritization issues. Also, note that Chrome is removing HTTP/2 Server Push, so if your setup depends on it, look toward Early Hints instead, which Fastly is already experimenting with.
Before optimising further, verify the basics:
- All assets run over a single HTTP/2 connection. A CORS misconfiguration or a bad
crossoriginattribute forces the browser to open a second connection. In DevTools → Network, add the "Connection ID" column and confirm every request shares one ID. - Your servers and CDNs actually support HTTP/2. Support varies. Check tools like CDN Comparison, and review Pat Meenan’s research on HTTP/2 prioritization. For reliable prioritization on Linux 4.9+ kernels, enable BBR congestion control and set
tcp_notsent_lowatto 16KB. - HPACK header compression is active. Some HTTP/2 servers don’t fully implement it. The h2spec tool can verify compliance.
- Security headers are in order. Since HTTP/2 runs over TLS in all browsers, ensure HSTS and Content Security Policy headers are set, plugins and scripts load via HTTPS, and the server passes an SSL Labs check.
It’s also worth checking HTTP/3 support. The IETF standardised HTTP/3, which builds on QUIC as the transport layer — replacing TCP with UDP-encapsulated packets. QUIC integrates TLS 1.3 directly, combining handshakes into a single round trip (or 0-RTT from the second connection). It also supports connection migration when you switch from Wi-Fi to cellular.
HTTP/3 solves TCP’s head-of-line blocking: with independent streams, one dropped packet affects only its own stream, not every request on the connection. Chrome, Firefox, and Safari all have implementations, and major CDNs and Google services (Analytics, YouTube) are already running on it. Neither Apache, nginx, nor IIS support it yet, but that’s likely to change through 2021. If your server and CDN can serve HTTP/3, the early results are promising.
Finding The Right Packaging Balance
Moving to HTTP/2 doesn’t automatically mean abandoning bundling. The ideal is a balance between delivering assets quickly and caching them well. Two extremes pull against each other:
- Many small modules — breaking the interface into separate files. A change in one file doesn’t invalidate the whole stylesheet or script, and parsing time per page stays low. But overall compression suffers because each file gzips in isolation, missing dictionary reuse across the whole package. Browser runtimes also aren’t fully optimised for hundreds of resources — Chrome triggers inter-process communication linearly with resource count.
- One large bundle — best for compression, but a single change forces re-downloading everything.
You can still serve CSS progressively — in-body CSS no longer blocks rendering in Chrome — though prioritisation quirks mean it’s worth experimenting with. Avoiding concatenation altogether isn’t necessary either; roughly 6-10 packages is a reasonable compromise that works even for legacy browsers.
HTTP/3 points toward a similar conclusion. Because its streams are independent, one paused stream slows only its own download. That makes many files in parallel attractive, but packaging still matters for compression and caching. The right choice depends on your site — measure first.
Testing, Monitoring, and Quick Wins
Testing should not be limited to Chrome and Firefox on a fast connection. Proxy browsers and legacy browsers matter, particularly in regions where they hold significant market share — UC Browser and Opera Mini, for example, account for up to 35% of mobile browsing in Asia. Check the average Internet speed in your target countries and test with appropriate network throttling. Remote real-device services like BrowserStack cover the breadth of devices, but keep a few physical devices in the office as well for hands-on verification.
Optimizing the Auditing Workflow
A well-configured testing setup saves hours over the long run. For quick checks, Tim Kadlec’s Alfred Workflow submits tests directly to the public WebPageTest instance. Reading WebPageTest’s Waterfall View and Connection View charts is worth learning to diagnose bottlenecks effectively. For automation, Lighthouse CI can fold accessibility, performance, and SEO scores into a Travis build, or you can integrate auditing into Webpack directly.
AutoWebPerf collects performance data from multiple sources automatically — for instance, pulling field data from the CrUX API and lab data from a PageSpeed Insights Lighthouse report. If a build feels slow, remember that whitespace removal and symbol mangling account for about 95% of size reduction in minified JavaScript; disabling compression speeds up Uglify builds by 3–4 times without affecting output quality.
Edge Cases Worth Testing: 404 Pages and Consent Prompts
404 responses deserve attention. Missing favicons, broken JavaScript, and absent font or manifest files generate 404 responses, and the response body for those pages is often surprisingly large. A 404 page served for every missing asset wastes bandwidth and can become a DoS vector if the origin has to answer every request. Cache the 404 page on a CDN so the edge responds instead. Include a 404 page in your Lighthouse suite and track its score over time.
GDPR and CCPA consent systems are often third-party scripts with their own performance cost. The impact varies with user behavior, so profile several scenarios: consent fully refused, partially refused, fully granted, and no action taken (possibly blocked by a content blocker). Consent prompts normally shouldn’t shift CLS, but sometimes they do. Free options like Osano or cookie-consent-box keep the overhead minimal. If you build custom popups, position calculations matter — Wikipedia’s page previews case study from the Wikimedia team is a useful reference for measuring that overhead.
Diagnostic CSS and Accessibility Timing
Tim Kadlec’s Performance Diagnostics CSS highlights common problems directly in the browser — lazy-loaded images above the fold, images without dimensions, legacy image formats, and synchronous scripts. It is easy to customize, for example to flag unused web fonts or icon fonts. It will not catch everything, but it makes low-hanging issues visible during debugging.
/* Performance Diagnostics CSS */
/* via Harry Roberts. https://twitter.com/csswizardry/status/1346477682544951296 */
img[loading=lazy] {
outline: 10px solid red;
}
Performance has an accessibility dimension that is easy to overlook. Screen readers maintain an accessibility tree alongside the DOM, and querying that tree takes time. Fast Time to Interactive for a screen reader user means how quickly navigation is announced and the keyboard becomes usable. Slow page loads and heavy DOM manipulation delay announcements. Screen readers are built for speed and can be less patient than sighted users, so test with JAWS, NVDA, and VoiceOver, not just Lighthouse.
Setting Up Continuous Monitoring
A private WebPageTest instance is useful for quick, unlimited tests, but continuous monitoring tools like Sitespeed, Calibre, and SpeedCurve provide the long-term picture. Add your own user-timing marks for business-specific metrics and automated regression alerts. RUM solutions track real user experience over time. Load-testing tools such as k6 handle automated, scriptable checks; SpeedTracker and Calibre complement Lighthouse for ongoing measurement.
Seventeen Quick Wins
If you have only an hour, start here. Establish goals first and measure before and after with LCP, FID, and TTI on both 3G and cable connections.
- Target at least 20% better performance than your fastest competitor: LCP under 2.5s, FID under 100ms, TTI under 5s on slow 3G and under 2s on repeat visits.
- Compress images with Squoosh, mozjpeg, guetzli, pingo, or SVGOMG; serve AVIF/WebP through an image CDN.
- Inline critical CSS for each template in its
<head>, keeping the total critical file budget at roughly 170KB gzipped. - Defer, lazy-load, and trim scripts; configure the bundler to remove redundancy.
- Self-host static and third-party assets where possible; use facades and load widgets on interaction.
- Choose frameworks carefully; prerender critical pages in SPAs and use progressive hydration.
- Use streaming SSR instead of client-side rendering when feasible; defer framework booting.
- Serve modern code with
<script type="module">and fallback to legacy bundles for older browsers. - Try regrouping stylesheets or placing critical CSS in the body.
- Add
dns-prefetch,preconnect,prefetch,preload, andprerenderhints where they help. - Subset web fonts, load them asynchronously, and use
font-displayfor fast first paint. - Verify HTTP cache and security headers.
- Enable Brotli compression; at minimum, enable Gzip.
- Enable TCP BBR on Linux kernels 4.9 and later.
- Use OCSP stapling and IPv6 if available.
- Enable HPACK for HTTP/2 and transition to HTTP/3 when supported.
- Cache fonts, styles, JavaScript, and images in a service worker.
Checklist Downloads And Alternatives
To put these recommendations into practice, the full checklist is available in three formats — a print-ready PDF, an editable Apple Pages document, and a Microsoft Word file — so you can adapt it to your own workflow and project needs:
- Download the checklist PDF (PDF, 166 KB)
- Download the checklist in Apple Pages (.pages, 275 KB)
- Download the checklist in MS Word (.docx, 151 KB)
If you prefer to build your own from another starting point, useful alternatives include the front-end checklist by Dan Rublic, the "Designer’s Web Performance Checklist" by Jon Yablonski, and the FrontendChecklist.
Putting The Checklist To Work
Not every optimization on the list will fit your budget, timeline, or legacy codebase — and that’s expected. Treat the checklist as a comprehensive starting point and filter it down to the issues that actually apply to your situation. The essential habit is to measure first, then optimize. Identify the bottlenecks in your own projects before making changes, and you’ll be set for a fast year ahead.



