DEV Community

Anthony KOZAK
Anthony KOZAK

Posted on Originally published at exoa.dev

Web Performance Budgets: Lessons From 16 Years in Games

After 16 years in game development, I see web performance differently from many web teams. Working as a Gameplay Programmer on Eagle Flight Arcade at Ubisoft Montreal in 2016 taught me to think in budgets, feedback loops, and worst-case behavior. Those ideas apply just as strongly to a product page, account portal, or browser-based application. A website does not need a frame counter to feel slow.

Key Takeaways
  • Treat loading time, interaction latency, JavaScript, and media as limited budgets.
  • Measure real user journeys instead of optimizing a single benchmark score.
  • Give every asynchronous interaction explicit loading, success, empty, failure, and cancellation states.
  • Design useful progress and graceful degradation before adding visual polish.
  • Automate performance checks, but validate important flows on physical devices.

Why Should Web Teams Think in Frame Budgets?

Games taught me that performance is not an abstract quality. It is a recurring deadline. At 60 frames per second, the application has roughly 16.7 milliseconds to process input, update its world, prepare rendering, and present the result. At 90 Hz, a common target in VR, that window drops to about 11.1 milliseconds. Miss the deadline repeatedly and the player feels it immediately.

Web applications have less obvious deadlines, but users still perceive them. A button should acknowledge input quickly. A route should reveal meaningful content before attention disappears. Scrolling should remain stable while images and advertisements arrive. Search results should not freeze the interface while filtering. The browser may not display a dropped-frame warning, yet hesitation and visual instability communicate the same message: the software is not keeping up.

I therefore prefer explicit budgets to requests such as make it fast. A project can define limits for initial JavaScript, image weight, font files, third-party scripts, server response time, and interaction latency. The exact limits depend on the audience and product. A public marketing page, an internal planning tool, and a rich browser editor should not inherit the same numbers without discussion.

The important part is forcing tradeoffs into the open. If a new analytics package consumes a meaningful part of the script budget, the team should decide whether its value justifies that cost. If an autoplaying video dominates the loading budget, that decision belongs in product review rather than being discovered during final optimization.

This mindset also changes sequencing. I do not build an unlimited experience and hope to optimize it later. I reserve capacity for features, identify the weakest realistic device, and test as the product grows. Performance becomes a design constraint like screen size, accessibility, or localization. That approach is less glamorous than a last-minute optimization sprint, but it is far more reliable.

What Should You Measure Before Optimizing a Web App?

The first performance mistake is measuring whatever a tool makes easiest rather than what matters to the user. A perfect score on an isolated page does not prove that account creation, checkout, file upload, or search feels good. Before changing code, I write down the important journeys and the moments where users need feedback. Those moments become the foundation of the measurement plan.

Core Web Vitals provide a useful shared vocabulary in 2025 and 2026. Largest Contentful Paint measures when the main visible content appears. Interaction to Next Paint measures responsiveness after user input. Cumulative Layout Shift measures unexpected movement. Google's good thresholds are an LCP within 2.5 seconds, an INP within 200 milliseconds, and a CLS of 0.1 or less, assessed at the 75th percentile.

Those metrics are a starting point, not a complete diagnosis. I also want server response time, transferred bytes, JavaScript execution time, long tasks, API error rates, and timing for product-specific actions. For an editor, opening and saving a document may matter more than the landing page. For an account system, authentication and form submission deserve dedicated instrumentation.

Laboratory and field measurements answer different questions. Lighthouse gives repeatable evidence during development, although scores can vary with configuration and environment. Real user monitoring reveals the devices, networks, routes, and regressions affecting actual visitors. Synthetic testing is controlled. Field data is representative. Mature teams need both rather than arguing that one invalidates the other.

I also segment results instead of averaging everything together. A fast desktop can hide a poor mobile experience, and a cached repeat visit can hide an expensive first load. Compare mobile with desktop, new visits with returning visits, and key routes with the site-wide summary. Optimization begins when the team can name the affected users, the delayed action, and the work responsible for that delay.

How Can Loading Screens Communicate Real Progress?

Game developers spend a great deal of time hiding or reorganizing loading. The same problem appears on the web whenever an application fetches records, initializes an editor, processes a payment, uploads a file, or waits for generated content. The worst response is often an unexplained spinner. It proves that animation still works, but it says nothing about whether the operation is progressing or stuck.

I prefer the earliest possible useful result. Render the page shell, heading, navigation, and cached information before optional content. Prioritize the image or data that establishes context. Defer analytics, recommendations, secondary panels, and decorative motion. This is similar to loading the playable or visible part of a game scene before preparing content the player cannot reach yet.

Progress indicators should reflect what the system knows. Use determinate progress for an upload when the client knows total bytes. Use named stages when the process has clear steps, such as uploading, validating, and processing. Use an indeterminate indicator only when progress cannot be estimated honestly. A fabricated percentage that stalls at 99 percent can damage trust more than a clear status message.

Skeleton layouts are useful when they preserve the final geometry, but they are not free performance. Complex shimmer effects can consume rendering time, and misleading placeholders create a second layout when the actual content differs. Reserve dimensions for media, keep placeholders simple, and replace each region independently. Partial success is usually better than holding the whole page behind one global loading state.

Finally, give slow operations an escape route. Users should be able to cancel an upload, navigate away safely, retry a failed request, or continue with another task when the workflow allows it. Loading design is not merely cosmetic. It defines how the product behaves under latency, which is why I treat it as part of application architecture rather than a final animation pass.

Why Does Every Async Interaction Need Explicit States?

Many web bugs come from pretending an asynchronous action has only two states: not started and done. Real operations can be idle, waiting, successful, empty, failed, cancelled, stale, or retrying. When those possibilities remain implicit, interfaces develop duplicate submissions, buttons that never re-enable, stale data that overwrites fresh data, and error messages that vanish before anyone can read them.

I learned to value explicit state machines through gameplay programming and through maintaining configurable Unity products such as Touch Camera PRO. A camera controller, input interaction, or network-backed interface becomes easier to reason about when transitions are named. The same model applies to React components, server-rendered forms, browser editors, and background jobs. Framework syntax changes, but state does not disappear.

public enum AsyncState
{
    Idle,
    Working,
    Succeeded,
    Empty,
    Failed
}

public sealed class AsyncOperationModel<T>
{
    public AsyncState State { get; private set; } = AsyncState.Idle;
    public T? Data { get; private set; }
    public string? Error { get; private set; }

    public async Task RunAsync(
        Func<CancellationToken, Task<T>> operation,
        CancellationToken token)
    {
        State = AsyncState.Working;
        Data = default;
        Error = null;

        try
        {
            Data = await operation(token);
            State = Data is null
                ? AsyncState.Empty
                : AsyncState.Succeeded;
        }
        catch (OperationCanceledException)
        {
            State = AsyncState.Idle;
        }
        catch (Exception ex)
        {
            Error = ex.Message;
            State = AsyncState.Failed;
        }
    }
}

This C# example is deliberately small. In production, I would separate a safe user-facing message from diagnostic details, record request identifiers, and define whether existing data remains visible during refresh. I would also prevent an older request from replacing a newer result. The important lesson is that rendering should follow a known state rather than infer one from a scattered combination of nullable values and Boolean flags.

Explicit states also improve product conversations. Designers can specify the empty view. Writers can prepare failure messages. QA can test cancellation and retry. Developers can decide whether an action is idempotent before enabling automatic recovery. A feature is not complete because its successful request works on a fast connection. It is complete when every meaningful transition has an intentional outcome.

How Can Asset Budgets Stop a Fast Site From Becoming Slow?

Most slow websites are not ruined by one spectacularly bad decision. They accumulate weight through ordinary additions: another font, a larger hero image, a chat widget, an experiment script, a component library, and duplicate utilities from separate packages. Each change appears acceptable in isolation. Together they consume the loading and execution budget that nobody documented.

I like budgets that assign ownership. Images need dimensions, appropriate formats, responsive variants, and a reason to load eagerly. Fonts need a subset strategy and sensible fallback. JavaScript needs route-level inspection, not just a total repository size. Third-party scripts need named owners because external code still uses the visitor's processor, network, memory, and privacy allowance even when it is not in your source repository.

A team might begin with a compressed JavaScript target for each public route, a maximum hero-image weight, and a limit on critical font files. Those numbers are not universal standards. They are conversation starters that should be validated against the supported device and network profile. A browser-based design tool may justify more code than a contact page, but it still benefits from lazy loading and deliberate feature boundaries.

The parallel with Unity asset development is strong. Touch Camera PRO has to provide flexibility without forcing every project to pay for every possible behavior at once. Web components should follow the same principle. Load advanced editors only where they are used. Avoid shipping an entire icon set for a handful of symbols. Do not initialize video, maps, or real-time connections before the user needs them.

Review dependencies as product decisions, not free implementation details. Check their parsed size, runtime behavior, update history, accessibility, and overlap with existing code. Native browser capabilities are often sufficient. When a dependency is justified, isolate it so replacement remains possible. The goal is not zero JavaScript or zero third-party services. The goal is knowing what each asset costs and why that cost belongs in the experience.

What Does Graceful Degradation Look Like in a Real Product?

Performance and resilience meet when something goes wrong. Networks become intermittent, APIs time out, authentication expires, browser storage fills, and third-party services fail. A fast happy path is valuable, but a product earns trust through its unhappy paths. Users should understand what happened, whether their work is safe, and what action they can take next.

Across freelance work that has included Meta Spirit Sling, Loreal Viva Tech 2024, Ticketly, and indie projects, the product contexts have varied considerably. The reusable lesson is not a particular framework. It is to identify critical capabilities and reduce the number of systems required to preserve them. Optional analytics should never block a primary action. A failed recommendation panel should not make account settings unavailable.

Forms deserve special care. Preserve entered data when submission fails. Disable duplicate submission while a request is active, but restore the control after failure. Use idempotency protection for operations that must not run twice. Validate near the input for fast feedback, then validate again on the server because the browser cannot be trusted as the final authority. If a session expires, explain whether the draft can be recovered before redirecting.

Retries should be selective. Retrying a transient read with exponential backoff can be reasonable. Blindly retrying a payment, deletion, or other side-effecting operation can create a worse failure. Respect server guidance, cap attempts, add jitter where many clients could retry together, and always provide a final state. An infinite spinner is not a recovery strategy.

Graceful degradation also includes accessibility and reduced capability. Core content should remain understandable without motion, with keyboard navigation, and at high zoom. Where practical, cache read-only information or drafts for intermittent connectivity. Not every application needs full offline operation, but every application should define what happens when connectivity disappears. Resilience becomes much easier when it is designed alongside the successful flow rather than attached after launch.

How Do You Make Performance Part of the Delivery Workflow?

Performance work fails when it depends on one enthusiastic developer remembering to run a test before release. The solution is not a larger optimization document. It is a delivery loop that makes regressions visible while they are still cheap to fix. I want performance discussed during planning, checked during implementation, reviewed in pull requests, and observed after deployment.

Start with a small set of representative routes and actions. Capture transferred bytes, script size, LCP, CLS, long tasks, and important product timings under a documented test profile. Add automated checks for clear violations, such as an unexpected bundle increase or an oversized image. Keep thresholds realistic enough to be enforced. A warning that everyone learns to ignore is not a budget.

Lighthouse CI and browser automation can catch valuable regressions, but synthetic scores have natural variance. Compare trends, run multiple samples, and investigate the underlying trace rather than treating one score as a verdict. For major releases, test on physical mobile hardware and a constrained network. Developer laptops often conceal main-thread, memory, thermal, and bandwidth problems that real users face.

After release, monitor field data by route, device class, geography where appropriate, and application version. Connect technical metrics to outcomes such as task completion and error recovery without collecting more personal data than the product needs. An improvement matters when it makes the intended journey more reliable, not merely when a dashboard turns green.

This workflow is part of how I approach web application and backend development. Architecture, interface behavior, testing, and deployment have to support the same performance targets. I also assign follow-up ownership. If a threshold fails, the team should know who investigates, which exceptions are allowed, and when temporary debt expires. Performance stays healthy when it becomes an ordinary acceptance criterion rather than a heroic cleanup phase.

What Should Web Teams Prioritize in 2026?

In 2026, web teams have more rendering strategies, hosting platforms, component libraries, AI coding tools, and observability products than they can reasonably evaluate. More options do not automatically produce faster software. In fact, the easiest way to lose performance is to stack abstractions before understanding the user journey they are supposed to support.

My priority order is simple. First, remove work that does not create user value. Second, delay work until it is needed. Third, move suitable work away from the main thread or critical request path. Fourth, cache results with an explicit freshness policy. Fifth, communicate the remaining wait honestly. This sequence usually produces better decisions than selecting a fashionable rendering mode and assuming architecture alone will solve latency.

AI-generated code makes discipline more important. A generated component can introduce a large dependency, duplicate an existing utility, trigger unnecessary requests, or mishandle cancellation while still looking convincing in review. I use automation to accelerate implementation, but generated output must obey the same budgets, security boundaries, accessibility requirements, and state model as handwritten code. Faster code production can otherwise mean faster performance regression.

I would also resist universal rules. Server rendering can improve the arrival of useful content, but it does not excuse excessive client hydration. Client rendering can support rich interaction, but it should not delay basic navigation unnecessarily. Edge execution can reduce distance for some workloads, but data location, consistency, cost, and operational complexity still matter. Measure the complete request rather than optimizing labels.

The biggest lesson I carry from games to the web is that responsiveness is part of design. Eagle Flight Arcade needed disciplined real-time behavior because VR makes delay difficult to hide. A web product may have a different tolerance, but users still notice when software ignores them. Define the budget, expose every meaningful state, test weak conditions, and keep measuring after release. Fast experiences are not created by one optimization. They are protected by hundreds of explicit decisions.

References & Further Reading

Top comments (0)