Skip to content

    Frontend System Design

    Performance and Cost Trade-Offs in Front-End Systems

    Learn to weigh speed against infrastructure cost, complexity and visitor cost, decide where a speed-up is worth buying, and know when to stop optimising.

    By Published 14 min read

    Share

    Standard lessonadvancedFrontend System Design

    Before this: Performance Budgets and Profiling

    What you'll learn: By the end of this lesson you will be able to name who pays for a speed-up (the visitor, your infrastructure bill, your team), estimate the cost of common front-end levers without inventing prices, use field data to decide where speed is worth buying and when to stop, verify a change against both a metric and a cost, and write down a performance-versus-cost decision someone else can review.

    A pizza shop wants faster delivery. It can hire more riders, buy a bigger map of side streets, add a second oven, or open a branch nearer the customers. Each makes pizzas arrive sooner, and each costs something: wages, rent, a more complicated kitchen. A sensible owner does not ask "how do we make it as fast as possible?". The owner asks where customers are waiting too long, what each fix costs, and when "fast enough" has been reached.

    A web page is the same. Faster is not free and not always worth it. The cost shows up in three places: in your cloud and CDN bill, in your team's time and in the number of things that can break, and on the visitor's own phone, where bytes and processing time have a real price. This lesson gives a way to compare them.

    The running example is the product page of an online store. Its team has a backlog of speed-ups: smaller hero images, a CDN for rendered HTML, a leaner JavaScript bundle, prefetching the next page and rendering at the edge. They cannot do everything, and some items cost more than they return.

    The core idea

    1. Every speed-up has a price in at least one of three ledgers. The visitor's ledger (milliseconds, bytes, battery), the platform ledger (money and risk in infrastructure), and the team ledger (effort to build, run and explain).
    2. Spend where field data shows users are slow, and stop when they are not. Web.dev's guidance is that field data is what you use to prioritise efforts. Once the 75th percentile is "good" for the segments that matter, more speed buys less.
    3. Cost and benefit are compared per user-visible result, not per technique. "Edge rendering" is not good or bad; "800 ms off phone LCP at this much more compute and complexity" can be judged.

    This is an application of the method in Trade-Off Analysis for Front-End Decisions to one recurring question. It assumes you can read field data; Real User Monitoring covers how it is aggregated and Loading Performance and Core Web Vitals the metrics.

    The three ledgers

    The visitor's ledger. JavaScript is not just bytes. The V8 team's analysis of the cost of JavaScript says that in 2019 "the dominant costs of processing scripts are now download and CPU execution time", that execution time matters for phones with slow CPUs, and gives one site's scripts taking several times longer on a low-end phone than on a high-end one. A long main-thread task is defined by web.dev as one over 50 milliseconds, and it hurts most on resource-constrained devices. So a bundle that is free on your laptop costs your phone visitors real time. A fast page is partly a smaller bill for them.

    The platform ledger. The bill depends on where work happens. web.dev's comparison of rendering approaches says server-side rendering has "compute overhead costs" and, when HTML is not cached, "can be slower than just serving static rendered content", while pre-generated pages can be deployed to multiple CDNs. CDN billing is described in terms of requests and data transfer: at the time of writing (read 2026-11-08) the CloudFront pricing page presents its plan allowances as requests, data transfer and included storage. This lesson quotes no prices; read your provider's current page and put your own figures in your own cost model.

    The team ledger. Each added layer is something to build, deploy, monitor and explain at 2 a.m. A service worker, an edge runtime or a second cache has its own failure modes, covered in Caching Across the Stack and Edge Rendering. This cost is the easiest to forget because it appears on no invoice.

    Figure 1. Where a request's cost lands

    A request path annotated with who pays for the work done at each stepVisitor device to CDN (request); CDN to Render server (miss); Render server to Data and APIsPAID BY THE VISITORPAID BY YOU, ON THE INVOICEVisitor device: CPU, battery, dataVisitor deviceCPU, battery,dataCDN: requests, transferCDNrequests,transferRender server: compute per renderRender servercompute perrenderData and APIs: queries, upstream callsData and APIsqueries, upstreamcallsrequestmiss

    How to read this: every box does work for each request that reaches it. The CDN answers most requests cheaply, the render server and the data tier only see misses, and the visitor's device pays for whatever the page asks it to download and run. A change that moves work to the right (more rendering on the server) lowers the visitor's cost and raises yours; the reverse is also true.

    Cost, benefit and the stopping rule

    A practical loop has five steps.

    1. Find the gap. From field data, list the segments (device class and page template) whose p75 misses the "good" target. web.dev gives those targets: LCP within 2.5 seconds, INP of 200 milliseconds or less, CLS of 0.1 or less.
    2. List candidate changes and, for each, the metric it moves, the segments it reaches and the share of traffic in them.
    3. Cost each in the three ledgers. Use ranges and units (engineer-days, renders per month, bytes per view), not a single dollar figure you cannot defend.
    4. Choose, ship behind a flag, and verify with both the metric and the cost.
    5. Stop when the p75 is good in the segments that matter, or when the next item costs more than it returns.

    Figure 2. The loop, with its stopping rule

    flowchart TD accTitle: Deciding where speed is worth buying accDescr: Start from field data per segment. If every important segment is good at the 75th percentile, stop and keep watching. Otherwise list candidate changes, cost each in three ledgers, pick the best benefit per cost, ship behind a flag, verify the metric and the cost, and repeat. F["Field data per segment"] --> Q{"p75 good in every
    segment that matters?"} Q -->|"yes"| S["Stop optimising,
    keep monitoring"] Q -->|"no"| C["List candidate changes"] C --> L["Cost each:
    visitor, platform, team"] L --> P["Pick best benefit per cost"] P --> R["Ship behind a flag"] R --> V{"Metric better and
    cost within budget?"} V -->|"yes"| F V -->|"no"| B["Roll back or revise"] B --> C

    How to read this: the loop is closed by field data, not by a feeling that the page is fast. The exit is on the left side and is reached by measurement. The rollback branch is a normal outcome, not a failure.

    Web.dev's guide to performance budgets states the same idea from the other side: a budget is "a set of limits imposed on metrics that affect site performance", it should be set per page type, enforced in the build, and complemented by real-user data after launch. When a change breaks the budget you can optimise it, remove something else, or decide not to add it. A budget is the place where performance and cost are compared before the code is written. See Loading Performance and Core Web Vitals for its three kinds of metrics.

    Three illustrations

    1. A minimal example: one image (illustrative numbers). A hero image is 300 KB and can be re-encoded to 180 KB at an acceptable quality. That saves 120 KB per view. At 10 million views a month (illustrative), the saving is 120,000 bytes x 10,000,000 = 1.2 TB of transfer. The visitor's ledger improves (fewer bytes, an earlier LCP), the platform ledger improves in transfer, and the team ledger is a one-off change plus a build step. All three move the same way, so this is an easy yes. Most real choices are not like this.

    2. A realistic example: render once or render per request. Rendering a product page on the server for every request is simple and always fresh. Suppose it is rendered per request, a CDN can cache the result, and the CDN cache hit ratio is the lever. The arithmetic below is for 10 million views (illustrative) and counts only what reaches the render server.

    CDN hit ratio (illustrative) Renders reaching the server per 10 million views
    99% 100,000
    95% 500,000
    80% 2,000,000

    Dropping from 99% to 80% multiplies server work by twenty. So the cost of "SSR with a CDN" is mostly the cost of keeping the hit ratio high, which depends on cache keys, lifetimes and purging, the subject of Caching Across the Stack.

    3. A failure case: speed bought on the wrong ledger. A team adds client-side prefetching of the next page for every link in view. Field LCP on desktop improves. On phones with metered data and slow CPUs it adds transfer the visitor did not ask for, and the extra script competes with interaction work, so INP gets worse in the segment that was already failing. The benefit was measured in the segment that was fine, and the cost landed on the one that was not.

    The levers and who pays

    Lever What it buys Where the cost lands Watch for
    Smaller, better-encoded images Fewer bytes, earlier LCP Build or transform step, maybe format support Quality loss; see images and fonts
    Less JavaScript, code splitting Less download and CPU on the device Team time, more requests to manage A split that moves cost to a later interaction; see code splitting
    Static or cached HTML at a CDN Low TTFB, little compute Build or revalidation complexity Stale pages and purge design
    Server rendering per request Fresh, personalised HTML Compute per render, a slower page when not cached Hit ratio collapse; hydration cost on the device
    Edge rendering Shorter distance to the visitor Runtime limits, data far from the edge Data locality; see edge rendering
    Longer cache lifetimes Fewer origin requests Staleness A staleness budget per data type
    Prefetching Faster next navigation Wasted bytes and CPU when wrong Metered and low-end devices
    Third-party tags Business features Main-thread time, bytes you do not control An owner and a budget for each tag

    Web.dev also warns about one popular hybrid: server rendering plus client "rehydration" is its own class of solution with very different trade-offs and "can have a significant negative impact on TBT and INP". The cost of rendering twice shows up on the visitor's device.

    Verifying a change on both ledgers

    Shipping a change without measuring both ledgers leaves you guessing. web.dev's field-measurement advice is to include a version identifier for each deployment and to set experiment groups on the server, not the client, so assignment does not delay rendering. It also says to defer analytics code and avoid long tasks so measuring does not degrade the page.

    Figure 3. Verifying a speed-up against a metric and a bill

    sequenceDiagram accTitle: Verifying a performance change against a metric and a cost accDescr: The server assigns a visitor to the new or old behaviour. Pages report metrics tagged with the release and group. The team compares p75 per segment and also compares cost signals such as renders and transfer, then decides to keep or roll back. participant S as Server participant P as Page participant R as RUM aggregator participant B as Cost signals participant T as Team S->>P: HTML for group A or B P->>R: metrics tagged with release and group S->>B: renders, transfer and cache hits per group T->>R: p75 per segment, group A versus B T->>B: cost per 1,000 views, group A versus B T->>T: keep, revise or roll back

    How to read this: the same visitors feed two comparisons. A change that wins on the metric but loses on cost is a decision for the team to make on purpose, with both numbers on the table.

    In practice: rendering a storefront at the edge

    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every number in it is an illustrative assumption.

    The situation

    A storefront's field data shows phone LCP at the 75th percentile above the 2.5 second target on product pages, while desktop passes. The largest share of the delay is the hero image and a slow origin response for uncached HTML. A proposal asks to render every product page at the edge so the server is near the visitor.

    Starting architecture

    Figure 4. The current path for a product page

    Starting architecture: a CDN in front of one origin that renders every product pagePhone to CDN (request); CDN to Origin renderer (miss); Origin renderer to Catalogue APIPhone: slow CPU, mobile networkPhoneslow CPU, mobilenetworkCDN: key includes all paramsCDNkey includes allparamsOrigin renderer: one regionOriginrendererone regionCatalogue API: same regionCatalogue APIsame regionrequestmiss

    How to read this: the warning box is the CDN cache key, which includes every query parameter. Any link with a tracking parameter makes a new cache entry and sends the request to the renderer.

    Options considered

    Option Benefit Cost Risk
    Render at the edge Shorter distance for HTML New runtime, data far from the edge, rework Database round trips from the edge may erase the gain
    Re-encode the hero image and size it correctly Fewer bytes on phones One build step Low
    Fix the cache key and lengthen HTML lifetimes with revalidation Fewer renders reach the origin Purge design Stale prices if budgets are not agreed

    The decision

    Field Decision record
    Context Phone p75 LCP misses the target; image bytes and uncached HTML are the biggest measured parts.
    Decision Re-encode and size the hero image, normalise the CDN cache key (drop tracking-only parameters), and serve HTML from the CDN with revalidation. Do not move rendering to the edge yet.
    Consequences, good Both ledgers improve for phones. The team ledger stays small: no new runtime.
    Consequences, bad A price change can be late by the agreed staleness budget.
    Revisit when Phone p75 LCP is still above the target after the hit ratio is high, and a profile shows the remaining cost is distance rather than rendering.

    Rollout and verification

    1. Baseline phone p75 LCP, the CDN hit ratio and renders per 1,000 views, by release.
    2. Ship the image change first, since it is the cheapest, and measure again.
    3. Change the cache key behind a flag and compare group A and group B on both the metric and renders per 1,000 views.
    4. Set alerts on hit ratio and on renders per minute, as part of the budget, not only on the metric.

    The 2 a.m. failure

    At 02:00 the render server's load triples and latency climbs. Field metrics for phones get worse a few minutes later. The CDN hit ratio has collapsed: a marketing email with a unique tracking parameter on every link, plus automated traffic adding random parameters, created a new cache key per request, so each one reached the renderer.

    First three checks: (1) the CDN hit ratio and the count of distinct URLs per page for the last hour; (2) the top query parameters on missed requests; (3) renders per minute against the baseline. Mitigation: drop the unknown parameters from the cache key at the CDN and rate-limit the offending source. Follow-up: keep an allow-list of parameters that change content, add an alert on renders per 1,000 views, and treat the cache key as part of the performance budget.

    Architect's notes

    • Name the segment before the technique. "Phone visitors on product pages" is a target; "SSR" is not.
    • Cost the team ledger honestly. A new runtime, a service worker or a second cache is operational debt even if the invoice does not change.
    • Prefer reversible changes first (images, cache keys, flags), as in trade-off analysis.
    • Put cost signals next to the metric on the same dashboard: renders, transfer and hit ratio per 1,000 views.
    • Write the stopping rule down. "Phone p75 LCP at or below 2.5 s for four weeks" ends a project that would otherwise run forever.
    • Re-check prices and plans in your provider's current documentation; this lesson gives units and arithmetic, not rates. The back-end side of the question, capacity and cost, is in Capacity Planning and Cost.

    Common mistakes

    • Optimising the segment that already passes.
    • Counting only the invoice. Visitor cost and team cost are real.
    • Comparing techniques instead of results. Compare milliseconds gained per unit of cost.
    • Shipping without a cost signal, so a metric win hides a bill that has doubled.
    • Letting the cache key include tracking parameters.
    • Treating a budget as a one-off. It must be enforced and re-read.
    • Promising revenue or ranking outcomes. Promise measured metrics.

    Key takeaways

    • Every speed-up is paid for in the visitor's ledger, the platform ledger or the team ledger.
    • Field data per segment decides where to spend; the "good" targets at p75 decide when to stop.
    • Compare options by user-visible result per unit of cost, using ranges and units, not made-up prices.
    • Server rendering's cost is mostly the cost of keeping a cache hit ratio high.
    • The visitor's device pays for JavaScript download and execution, and that falls hardest on low-end phones.
    • Verify with both a metric and a cost signal, using server-assigned groups and release tags.
    • Write down the decision, the cost and the condition under which you will revisit it.

    Practice exercises

    Exercise 1, Starter (30 min): For a page you know, list five possible speed-ups and, for each, which of the three ledgers it moves and in which direction.

    Exercise 2, Intermediate (2 hours): Take 10 million monthly views (illustrative) and compute renders reaching an origin at hit ratios of 99%, 97%, 90% and 70%. Then write one paragraph on what would make the real ratio fall.

    Exercise 3, Advanced (half day): Write a decision record for "render at the edge" versus "cache HTML at the CDN" for a page with personalised prices. State the metric, the cost signals, the staleness budget and the condition that would make you revisit it.

    In this course:

    Elsewhere on the site:

    Official documentation

    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