Skip to content

    Frontend System Design

    Loading Performance and Core Web Vitals

    Learn what LCP, INP and CLS measure, how lab and field data differ, which design choices move each metric, and how to build a real-user monitoring pipeline.

    By Published 18 min read

    • frontend-system-design
    • core-web-vitals
    • performance
    • real-user-monitoring
    Share

    This lesson is part of Frontend System Design Foundations: How the Web Runs. Enroll for free, then sign in, to read it in full.

    What you'll learn: By the end of this lesson you will be able to explain what Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift measure and how they are judged, tell lab data from field data, name the architecture decisions that move each metric, write a performance budget, measure the metrics in the browser, and design a small real-user monitoring pipeline.

    Think of a restaurant. You care how long until the main dish arrives, whether the table wobbles while you eat, and how quickly the waiter reacts when you raise a hand. A kitchen can be proud of its average service time and still lose customers, because customers remember the slow evening.

    A web page has the same moments, and the Core Web Vitals put numbers on the ones users feel most. The running example is the product page of an online store: a shopper arrives from search, waits for the main photo, taps "Add to basket", and expects the page to hold still and respond.

    The thresholds and API facts below come from web.dev, MDN and the web-vitals README as published in November 2026. Definitions change, so check the linked pages before you put numbers in an alert.

    The core idea

    Performance work has two halves. Measurement decides what to measure, on whom, and at which percentile. Design is the set of architecture decisions (what the server returns, what the browser fetches first, what runs on the main thread) that move the numbers.

    At the time of writing, web.dev lists three Core Web Vitals: Largest Contentful Paint (loading), Interaction to Next Paint (responsiveness) and Cumulative Layout Shift (visual stability). INP became a stable Core Web Vital in 2024 and replaced First Input Delay, which measured only the input delay of the first interaction.

    A page passes when it meets the "good" target for all three at the 75th percentile of page loads, segmented across mobile and desktop (web.dev: Web Vitals). A percentile answers "what value did most visits stay within?": at the 75th, three visits in four were at that value or better. It is tolerant of a slow tail, but stricter than an average.

    The three metrics

    Metric Question it answers Good Needs improvement Poor
    LCP When did the main content appear? 2.5 s or less in between more than 4.0 s
    INP How fast does the page respond when used? 200 ms or less up to 500 ms more than 500 ms
    CLS Does the layout stay put? 0.1 or less in between more than 0.25

    Source: each metric's own web.dev page, linked above.

    Largest Contentful Paint (LCP)

    LCP reports when the largest visible element in the viewport finishes rendering, timed from the start of navigation. It stands for "the main content has probably loaded"; on the product page, usually the hero photo. Candidates, per web.dev, are images, video posters, elements with a CSS url() background, and block-level text; reporting stops at the first tap, scroll or key press. Because it starts at navigation, LCP includes redirects, DNS, connection setup and server time.

    Interaction to Next Paint (INP)

    INP observes clicks, taps and key presses (not hover, scroll or zoom) across the visit. For each, it measures the time from the input until the next frame showing a response is painted. The page's INP is its worst interaction, ignoring one outlier per 50 interactions, reported when the user leaves.

    An interaction has three phases: input delay (waiting for a busy main thread), processing duration (your event handlers) and presentation delay (style, layout and paint until the frame is on screen).

    sequenceDiagram accTitle: The three phases of one interaction accDescr: A shopper taps Add to basket. The main thread may be busy first (input delay), then runs handlers (processing duration), then the browser renders the next frame (presentation delay). INP is the time from the tap to that frame. participant S as Shopper participant M as Main thread participant D as Display S->>M: tap "Add to basket" Note over M: input delay (busy with another task) M->>M: run click handlers (processing duration) M->>D: style, layout, paint (presentation delay) D-->>S: basket count visibly updates

    How to read this: INP is the whole time from tap to updated pixels. A long task already running makes the first phase long even when your handler is tiny.

    Cumulative Layout Shift (CLS)

    A layout shift is a visible element moving between two frames. Its score is the impact fraction (share of the viewport covered by the moving elements across both frames) times the distance fraction (the biggest move as a share of the viewport's larger dimension). Web.dev's example: 75 percent of the viewport moving 25 percent of its height scores 0.75 x 0.25 = 0.1875.

    CLS is not the sum of all shifts. Shifts are grouped into session windows: a shift joins the current window if it comes less than one second after the previous one, and a window lasts at most five seconds. CLS is the worst window. Shifts within 500 ms of a discrete input (click, tap, key press) are excluded.

    Supporting metrics for diagnosis

    These say where the time went. They are not pass/fail targets.

    Metric What it measures Core Web Vital? Lab or field
    Time to First Byte (TTFB) Navigation start to first response byte, including redirects, DNS and TLS. Web.dev's rough guide: 0.8 s or less is good No Both
    First Contentful Paint (FCP) Navigation start to first text, image, SVG or non-white canvas. Good is 1.8 s or less Not listed as one on its web.dev page Both
    Total Blocking Time (TBT) After FCP, the sum of each long task's time beyond 50 ms No Lab. Web.dev says to measure it in the lab; it is a proxy for INP, "not a substitute"

    A long task is main-thread work over 50 ms. The browser cannot interrupt it, so input arriving meanwhile waits. Web.dev's example: tasks of 250, 90, 155, 35 and 30 ms block for 200, 40, 105, 0 and 0 ms, so TBT is 345 ms; it suggests aiming under 200 ms on average mobile hardware. TTFB sets the earliest moment anything can render, but web.dev notes a server-rendered page with a higher TTFB can still beat a client-rendered one on FCP and LCP.

    Lab data versus field data

    Lab data is a controlled run on a fixed device and network, usually in Lighthouse. Field data is real visits.

    Lab (Lighthouse) Field (CrUX, RUM)
    Who is measured One simulated visit Real visitors
    Good for Debugging, pre-release checks, slow-device tests What users experience; which pages to fix
    Cannot tell you How real devices, caches and behaviour differ Why a number is bad; includes only visits that loaded
    INP Cannot predict when users interact (TBT proxies it) Measured directly

    The Chrome User Experience Report (CrUX) is a public dataset of real Chrome users' experience: a rolling 28-day aggregate giving the share of visits rated good, needs improvement or poor, and a 75th percentile. It covers only Chrome, and a page or origin needs enough traffic to appear. Real-user monitoring (RUM) is your own collection of the same metrics from your own visitors, built later in this lesson.

    Web.dev's lab and field guide explains why they disagree: the LCP element varies with screen size, personalisation and A/B tests; caches and the back/forward cache (bfcache) speed up real visits; field CLS includes shifts after scrolling. Its advice: prefer field data when you have it, and use lab data to debug and to test slow devices.

    What architecture decisions move each metric

    Figure 1. Where each metric is decided on one product page load

    Where each metric is decided on one product page loadBrowser to CDN edge (HTML, images); CDN edge to Origin server (on a miss); Browser to Third-party scripts (scripts)Browser: LCP, INP, CLS happen hereBrowserLCP, INP, CLShappen hereCDN edge: cached HTML, imagesCDN edgecached HTML,imagesOrigin server: builds HTML, TTFBOrigin serverbuilds HTML, TTFBThird-party scripts: tags, chatThird-partyscriptstags, chatHTML, imageson a missscripts

    How to read this: the path from browser to origin decides TTFB and the start of LCP; the browser box is where CLS and INP happen; the muted third-party box is outside your control but inside your metrics.

    Figure 2. The hero image on its way to becoming the LCP element

    sequenceDiagram accTitle: Loading the hero image of the product page accDescr: The browser requests the page from the CDN edge, which fetches HTML from the origin on a miss. After the first byte arrives, the browser parses the HTML, discovers the hero image, requests it, and paints it, which is the LCP candidate. participant B as Browser participant C as CDN edge participant O as Origin B->>C: GET product page C->>O: cache miss, fetch HTML O-->>C: HTML C-->>B: first byte (TTFB ends) Note over B: parse HTML,
    discover hero image B->>C: GET hero image C-->>B: image bytes Note over B: paint hero image
    (LCP candidate)

    How to read this: the four parts of LCP appear in order: waiting for the first byte, the gap before the image request starts, the download, and the time to paint.

    Web.dev's LCP optimisation guide splits LCP into exactly those four parts: time to first byte, resource load delay, resource load duration and element render delay. It gives rough proportions (about 40 percent, under 10, about 40, under 10) and warns against turning them into absolute time targets. Use them to find the largest part first.

    Figure 3. Reading a bad field LCP

    flowchart TD accTitle: Deciding where to look when field LCP is poor accDescr: Starting from a poor field LCP, find the largest of the four LCP parts and follow the branch to the matching kind of fix. A["Field LCP poor at p75"] --> B{"Which LCP part is largest?"} B -->|"first byte"| C["Server work, caching, CDN, redirects"] B -->|"load delay"| D["Make image discoverable and prioritised"] B -->|"load duration"| E["Fewer bytes, right size and format, nearer"] B -->|"render delay"| F["Render-blocking CSS and JS, long tasks"]

    How to read this: choose the branch by measurement, not habit. Shrinking image bytes does nothing if the time goes on waiting for the server.

    Resource priority and the critical path

    The critical path is the chain of requests the browser must finish before it can paint the main content. Keep it short and make the LCP resource discoverable in the initial HTML: an <img> in the markup is found by the browser's early scan, while an image whose URL only arrives after a JavaScript bundle runs starts late. Web.dev recommends fetchpriority="high" on the likely LCP image (a hint that browsers may override), never lazy-loading it, and keeping high priority for only a few images.

    <img src="/img/boot-1200.jpg" srcset="/img/boot-640.jpg 640w, /img/boot-1200.jpg 1200w"
         sizes="(min-width: 900px) 600px, 100vw"
         width="1200" height="900" alt="Brown leather boot, side view"
         fetchpriority="high">
    <img src="/img/boot-sole-640.jpg" width="640" height="480" alt="Boot sole detail" loading="lazy">
    <script defer src="/js/product.js"></script>
    <script async src="https://tags.example.com/loader.js"></script>

    The hero has width and height (reserves space), fetchpriority="high" and no loading="lazy". defer downloads in parallel and runs after parsing, in document order; async runs as soon as the script arrives, in no guaranteed order, which suits independent scripts (MDN: script element).

    Server response time, caching and CDN

    TTFB is the floor under LCP. The decisions are architectural: whether HTML can be served from a cache near the user, how many redirects precede the page, and how much work the origin does before the first byte. Web.dev also notes that unique query-string parameters (analytics ones, say) can make a CDN treat identical content as different, so the cached copy is not used. The browser side is HTTP Caching for the Front End; the server side is caching basics and CDN and object storage. Which rendering strategy produces the HTML changes TTFB and image discovery; see Rendering Strategies.

    Image sizing, formats and fonts

    Images are often the LCP element and the largest download: serve the size the slot needs (srcset, sizes), a modern format with a fallback, and compress. For fonts, web.dev's font guide says font-display: optional has a short block period and no swap, so a late font is skipped and cannot shift the layout, while swap shows fallback text at once and swaps later. It also notes size-adjust in @font-face can bring a fallback font closer to the web font's size, reducing CLS at the swap.

    Layout stability: reserve space

    Every CLS fix says "tell the browser how big this will be before it arrives". Web.dev's CLS guide recommends width and height or aspect-ratio for images and video, min-height or aspect-ratio for ad and embed slots, placeholders for late content, and animating transform rather than layout properties.

    .gallery { aspect-ratio: 4 / 3; }      /* hero space exists before the image loads */
    .reviews { min-height: 320px; }        /* illustrative: reserve the widget's expected size */
    .promo-slot { min-height: 56px; }      /* illustrative: a banner gets a slot, filled or empty */
    .drawer { transform: translateX(100%); transition: transform 200ms; } /* transform, not left/top */

    Main-thread work, long tasks and third parties

    The main thread runs your JavaScript and the browser's style, layout and paint work. Long tasks delay input handling (INP's first phase), delay painting the LCP element after its resource has loaded, and raise TBT. Web.dev's INP guide recommends yielding so input can be handled between chunks of work (it names setTimeout), avoiding forced layout, and keeping the DOM small. The Event Loop and the Main Thread explains why a task cannot be interrupted.

    Third-party scripts (tags, reviews, chat, A/B tests) share that thread and network. Treat each as a design decision with an owner, a loading strategy, a reserved slot if it renders UI, and a way to switch it off.

    Decision Mainly moves
    HTML cached at the edge LCP (first byte)
    Hero image in HTML, high priority LCP (load delay)
    width/height, aspect-ratio, reserved slots CLS
    Code splitting, yielding, less work per click INP, and LCP render delay
    Third parties loaded late, slot reserved INP, LCP contention, CLS
    font-display: optional or matched fallback CLS

    A performance budget is a design artefact

    A performance budget is a set of agreed limits on what affects speed. Web.dev's budget guide names three kinds (quantity-based limits on bytes and counts, milestone timings, rule-based scores) and advises combining quantity limits with user-centric metrics, setting them per page type, and enforcing them in the build and after launch. When a change breaks the budget, you optimise something, remove something, or do not add it.

    Every figure in this table is illustrative; set yours from your own field data.

    What Example budget (illustrative) Enforced by
    Metric targets LCP, INP, CLS "good" at p75, mobile and desktop RUM dashboard and alerts
    Hero image size 150 KB Build check on image files
    JavaScript on the product route 200 KB compressed Bundle-size check in CI
    Third-party requests 5 Review of the tag list
    Main-thread work in a lab load TBT under 200 ms Lighthouse CI

    Quantity budgets warn early in CI; metric budgets are the truth but need field data. Use both.

    Measuring in the browser

    The browser exposes data through the Performance Timeline: register a PerformanceObserver for an entry type and it calls you as entries are recorded (MDN). Use type with buffered: true to also receive entries recorded before the observer existed; entryTypes cannot be combined with buffered. The relevant types are "largest-contentful-paint", "layout-shift" and "event" (plus "first-input"). Support differs by browser and evolves, so test PerformanceObserver.supportedEntryTypes at runtime; MDN marks LayoutShift as limited availability.

    // Minimal LCP observer: the last entry received is the current candidate.
    if (typeof PerformanceObserver !== "undefined" &&
        PerformanceObserver.supportedEntryTypes.includes("largest-contentful-paint")) {
      let lcp = null;
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        lcp = entries[entries.length - 1];
      }).observe({ type: "largest-contentful-paint", buffered: true });
    
      addEventListener("visibilitychange", () => {
        if (document.visibilityState === "hidden" && lcp) {
          console.log("LCP (ms):", lcp.startTime, "element:", lcp.element, "url:", lcp.url);
        }
      });
    }

    This teaches the API but is incomplete. Web.dev's LCP page lists what a correct version must handle: stopping at the first tap, scroll or key press, bfcache restores (not reported by the API, though LCP should be measured), and prerendered pages (measure from activationStart). CLS and INP are harder: summing layout-shift entries that lack hadRecentInput is not CLS, because CLS uses session windows, and INP needs Event Timing entries grouped by interactionId. Use the web-vitals library (npm install web-vitals), which implements these rules: call onLCP, onINP, onCLS or onTTFB with a callback, as in the pipeline code below. Each callback gets a metric object with name, value, rating ("good", "needs-improvement" or "poor"), delta, id (unique per page view and metric, for de-duplication), entries and navigationType. At the time of writing the README documents version 5; onCLS is Chromium only, while onFCP, onINP, onLCP and onTTFB work in Chromium, Firefox and Safari. A web-vitals/attribution build adds an attribution property that breaks LCP and INP into the parts described above, to find causes.

    A small real-user monitoring pipeline

    Collecting numbers is easy. The design questions are what to send, how much, where, and how to turn it into a percentile.

    Figure 4. A real-user monitoring pipeline for the product page

    Real-user monitoring pipeline from browser to dashboardProduct page to Collector endpoint (sendBeacon); Collector endpoint to Queue; Queue to Aggregator; Aggregator to Metrics store; Metrics store to Dashboards, alertsProduct page: web-vitals, sampledProduct pageweb-vitals,sampledCollector endpoint: validate, limitCollectorendpointvalidate, limitQueue: absorbs burstsQueueabsorbs burstsAggregator: histograms, p75Aggregatorhistograms, p75Metrics store: per template, dayMetrics storeper template, dayDashboards, alerts: p75 vs targetsDashboards,alertsp75 vs targetssendBeacon

    How to read this: dashed arrows are fire-and-forget. The page never waits for the collector, and the collector never waits for the aggregator.

    1. Collect. Record name, value, rating, id and navigationType, plus the dimensions you will slice by: page template (/product/:slug, not the raw URL), device class, release. Send no personal data.
    2. Sample. Decide once per page view, so a visit reports a coherent set. A low rate saves cost; a high rate gives rare segments enough data.
    3. Send with navigator.sendBeacon. It queues a small POST that the browser delivers without delaying unload. MDN says the reliable moment is when visibilityState becomes "hidden"; unload is unreliable and defeats the bfcache. Queued data is limited to 64 KiB (MDN).
    4. Aggregate by percentile. Store counts in value buckets (histograms) per template and day, and compute p75 from merged buckets. Never average percentiles.
    5. Alert when p75 passes the "good" threshold, per template and device class.
    import { onCLS, onINP, onLCP, onTTFB } from "web-vitals";
    
    const SAMPLE_RATE = 0.1; // illustrative: report one page view in ten
    
    if (Math.random() < SAMPLE_RATE) {
      const queue = [];
    
      const enqueue = ({ name, value, rating, id, navigationType }) => {
        queue.push({ name, value, rating, id, navigationType, template: "/product/:slug" });
      };
    
      const flush = () => {
        if (queue.length === 0) return;
        navigator.sendBeacon("/rum", JSON.stringify(queue.splice(0)));
      };
    
      onLCP(enqueue);
      onINP(enqueue);
      onCLS(enqueue);
      onTTFB(enqueue);
    
      document.addEventListener("visibilitychange", () => {
        if (document.visibilityState === "hidden") flush();
      });
    }

    JSON.stringify gives a string, so the server parses the text body as JSON. The README's batching example flushes on the same event. Check in the field that the final INP and CLS values arrive, and deduplicate by id on the server.

    A tiny percentile example with illustrative LCP values in seconds for five visits: 1.5, 1.8, 2.0, 2.6, 3.9. By the nearest-rank method p75 is the value at rank ceil(0.75 x 5) = 4, which is 2.6 s and misses the 2.5 s target; the mean, 2.36 s, looks like a pass. Percentile tools differ in method, so state yours. For histograms and alerting on user-visible symptoms, see observability and SLOs: the Core Web Vitals targets act as front-end service level objectives.

    Talking about performance with product teams

    Performance is a product decision with a cost, so use the product's terms.

    • Speak in visits, not milliseconds. "Three in four product-page visits show the main photo within 2.5 seconds" is understood by everyone.
    • Make trade-offs explicit. A new widget costs bytes, main-thread time and layout space; ask it to fit the budget, or for something else to leave. A promo banner needs a reserved slot; a tracking tag needs an owner and a switch.
    • Do not promise a ranking or revenue result. Say what you will measure and show the numbers.

    In practice: speeding up an online store's product page

    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common patterns, and every number in it is an illustrative assumption.

    The situation

    An online store has a React single-page application, owned by a team of eight. Field data shows product-page LCP at p75 of 3.6 s and CLS of 0.18 (both illustrative); INP is good. Lighthouse in CI looks healthy, because the CI machine is fast and the lab run never receives the marketing team's A/B tests.

    Starting architecture

    Figure 5. Starting architecture: the browser builds the page after the JavaScript arrives

    Starting architecture: client-rendered product page with late third-party widgetsBrowser to CDN (bundle, images); Browser to Product API (product JSON); Browser to Tag manager (tag script); Tag manager to Reviews, chat widgets (injects)Browser: renders after JS runsBrowserrenders after JSrunsCDN: JS, CSS, imagesCDNJS, CSS, imagesProduct API: JSON, not cachedProduct APIJSON, not cachedTag manager: marketing owns itTag managermarketing owns itReviews, chat widgetsReviews, chatwidgetsbundle, imagesproduct JSONtag scriptinjects

    How to read this: the hero image's URL is in the JSON from the API, so the browser cannot request it until the bundle has run and the API has answered.

    The HTML is an empty shell, so image discovery is late (a large resource load delay), and the reviews widget is injected with no reserved slot, pushing "Add to basket" down.

    Options considered

    Option Helps Costs and risk
    Tune only: compress images, trim the bundle Load duration Cheap, but does not fix late discovery
    Server-render the product page; cache HTML at the edge First byte, discovery, render delay Rendering infrastructure; stale price if invalidation fails
    Rebuild as fully static pages First byte Large; slow builds; price freshness

    The decision

    Field Decision record
    Context Late discovery of the hero image and unreserved widget space dominate field LCP and CLS.
    Decision Server-render the product route with the hero image in the HTML (fetchpriority="high", not lazy). Cache HTML at the edge for a short time; fetch price and stock separately. Reserve space for gallery and reviews. Load tags with async under a budget.
    Consequences, good The hero image is requested at the first parse; layout is stable; field data drives release decisions.
    Consequences, bad More server work and a cache to invalidate; hydration cost to watch against INP.
    Revisit when Field p75 for any of the three is not "good" two weeks after release.

    Rollout and verification

    1. Ship the RUM pipeline (Figure 4) first, tagged by template and release, for a baseline.
    2. Serve the server-rendered route to a small share of traffic and compare p75 between groups; watch TTFB.
    3. Add budget checks and a Lighthouse run to CI, and review the LCP attribution parts weekly.

    The 2 a.m. failure

    At 02:00 an A/B test published through the tag manager injects a promo banner above the product title for some visitors. CI stays green because the lab run never receives the experiment. The RUM alert on CLS p75 for the product template crosses the "good" threshold within hours. First checks: filter by release and device, read the sources of the layout-shift entries to see which element moved, and check the tag manager's change log. Mitigation: use the tag's kill switch, then relaunch with a reserved slot. Follow-up: add budget review to the tag release process and a lab test with experiments enabled.

    Architect's notes

    • Fix the measurement first: metrics, percentile, device segments and page templates shape what you collect.
    • Make the LCP element a conscious choice per template: what it is, where it is discovered, who owns it.
    • Reserve space by rule: any late-arriving element gets a slot of known size.
    • Budget and own third parties, and give each a switch.
    • Defer micro-tuning until field data says where the time goes.

    Common mistakes

    • Trusting Lighthouse alone. One simulated visit is not your audience.
    • Reporting averages. They hide the slow tail; use p75 and histograms.
    • Lazy-loading the hero image, which adds load delay to LCP.
    • Images without dimensions, so layout shifts when they arrive.
    • Sending data on unload. Use visibilitychange with sendBeacon.
    • Treating TBT as INP, or averaging percentiles instead of merging counts.

    Key takeaways

    • Three Core Web Vitals at the time of writing: LCP, INP and CLS; INP replaced FID in 2024.
    • Judged at p75 of page loads, by device class: good is LCP 2.5 s or less, INP 200 ms or less, CLS 0.1 or less.
    • LCP has four parts: first byte, load delay, load duration, render delay. Find the largest first.
    • TTFB, FCP and TBT are diagnostics; TBT is lab, INP needs field data.
    • Lab finds causes, field shows truth.
    • Most wins are architectural: discoverable, prioritised hero image; cacheable HTML; reserved space; less main-thread work; owned third parties.
    • A budget is a decision rule: quantity limits warn early, metric targets are the truth.
    • Use web-vitals, send on visibilitychange with sendBeacon, aggregate counts into percentiles.

    Practice exercises

    Exercise 1 — Starter (30 min): In Chrome DevTools, find the LCP element of a page you know and guess which of the four LCP parts is largest. Check with a Lighthouse run.

    Exercise 2 — Intermediate (2 hours): Add web-vitals to a small page and log the metrics. Throttle network and CPU, add an image without width and height, and observe LCP and CLS. Fix it and repeat.

    Exercise 3 — Intermediate (2 hours): Write a one-page performance budget for a page type you own, marking which lines CI enforces and which need field data, with an owner for each.

    Exercise 4 — Advanced (half day): Build the Figure 4 pipeline locally: a page that sends sampled metrics by sendBeacon to a small endpoint that stores histogram buckets, and a script that prints p75 per template.

    In this course:

    Elsewhere on the site:

    Official documentation

    Enroll to keep reading

    Lesson 8 of 8 · Frontend System Design Foundations: How the Web Runs

    Learn what LCP, INP and CLS measure, how lab and field data differ, which design choices move each metric, and how to build a real-user monitoring pipeline.

    In the rest of this lesson

    • What architecture decisions move each metric
    • A performance budget is a design artefact
    • Measuring in the browser
    • A small real-user monitoring pipeline
    • Talking about performance with product teams
    • In practice: speeding up an online store's product page

    Enrolling is free. It unlocks every lesson in Frontend System Design Foundations: How the Web Runs, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments