Frontend Engineering

Next.js application engineering

This is the application rather than the architecture: routing, how each route renders, where its data comes from, and what it does while that data is arriving or failing.

The problem

A Next.js application is a set of per-route decisions

Next.js will run an application that has not made those decisions. Every route then renders the same way, fetches data the same way and fails the same way, and that uniformity is what makes the application slower than it needs to be in one direction and staler than it should be in the other. The framework offers several rendering and caching modes and the application's quality is largely the quality of choosing between them per route.

  • Every route renders the same way because that was the default
  • The site is fast for some pages and inexplicably slow for others
  • Content goes stale or takes too long to appear after it changes
  • Loading states are inconsistent or absent
  • An error in one component takes down the whole route
  • Data fetching happens on the client for content that never changes
  • The bundle is large and nobody can say what is in it
  • The application was built and its rendering strategy was never discussed
Who this is for

The people who usually bring us this problem

A team with a Next.js application that needs work

It functions and its per-route behaviour was never chosen, and the symptoms are showing up as speed and staleness.

A team building on Next.js

You want the rendering and data decisions made deliberately rather than defaulted into.

A team whose frontend has grown beyond its original design

The application started small and the structure has not kept up with what it now does.

What it costs

What this costs while it goes unfixed

Engineering faults are rarely confined to the engineering layer. These are the commercial consequences we see most often.

The rendering mode is a per-route decision and the default is not it

Static, server-rendered, incrementally regenerated, edge-rendered and client-rendered each suit different routes on the same site. Choosing one globally is simpler to configure and worse at both ends: the pages that could be static are generated per request, and the pages that need freshness are cached.

Caching has layers and they interact

Next.js caches fetches, routes and full responses, and the platform underneath caches again. A page can be stale for a reason three layers down, and the diagnosis is only possible if each layer's behaviour is known. Most staleness complaints resolve to a fetch cache nobody intended to enable.

What a crawler receives depends on the rendering choice

A statically rendered route delivers complete HTML; a client-rendered one delivers a shell. Both can be correct and they are different search propositions, which is why the rendering decision is made with the search consequence known rather than after it has been measured.

Error and loading states are part of the application

A route that has no error boundary takes the whole page down when one component fails, and a route with no loading state shows nothing while its data arrives. These are not polish; on a production frontend they are the difference between a degraded page and an outage.

What we do about it

Capabilities

Each of these is work we carry out, not an area we advise on.

Routing structure

How routes are organised, what is shared between them, where layouts apply and how dynamic segments are used. The structure determines what can be cached, what re-renders and how much of the application is sent for any given page.

Rendering mode per route

Each route classified and given the mode that suits it: static, server, incremental, edge or client. Decided against how often the content changes, whether it varies per visitor, and what a crawler needs to receive.

Data fetching and caching

Where each route's data comes from, how long it is valid, and what invalidates it — with the framework's own caching and the platform's cache considered together rather than separately. This is where most staleness and most of the unnecessary origin load originate.

Component and state structure

What is a server component and what is a client component, what state lives where, and how much of the application is actually sent to the browser. The boundary between the two determines the bundle size and the amount of hydration work.

Loading and error behaviour

Per route: what is shown while data arrives, what is shown when it fails, and what happens when a single component throws. Boundaries placed so a failure degrades a section rather than the page.

Asset and bundle management

What is in the bundle, what is loaded per route, and what is deferred. Measured rather than assumed, because a bundle grows by accretion and the largest contributor is frequently a dependency nobody remembers adding.

Build and deployment

How the application is built, what is pre-rendered at build time, how a deploy behaves for a visitor mid-session, and how content changes reach a running deployment. An application whose deployment is not understood cannot be operated.

Search-engine delivery

What each route serves before JavaScript runs, structured data, metadata per route, canonical URLs and sitemaps. Verified on the output rather than the intent, because a rendering mode change alters all of it.

How we work

Engineering methodology

The sequence is deliberate. The order is usually what determines whether the work holds or has to be repeated.

  1. Classify every route before choosing anything

    How often its content changes, whether it varies per visitor, and what a crawler needs from it. The rendering and caching decisions follow from those three questions, and making them globally is what produces a site that is both slower and staler than it should be.

  2. Choose the rendering mode per route, explicitly

    And record the choice with its reason, because the reason is what a future change is measured against. A route that is server-rendered for a stated reason is one that can be reconsidered; one that is server-rendered by default cannot.

  3. Understand every cache layer

    The fetch cache, the route cache and the platform cache, each with its own lifetime and invalidation. Staleness is diagnosed by knowing which layer is holding the old value, and unnecessary origin load by knowing which layer should have answered.

  4. Place boundaries so failures degrade rather than cascade

    Error and loading states at the level where a failure should be contained. A frontend without them is one where a single component's error is a blank page, which is a worse outcome than the error itself.

  5. Measure the bundle rather than reasoning about it

    What is sent per route, what is deferred, and what each dependency costs. Bundles grow by accretion and the largest contributor is rarely the one anybody suspects.

  6. Verify what is delivered, not what is intended

    The HTML before JavaScript runs, the metadata, the structured data and the redirects, checked on the response. A rendering change alters all of them, and the only way to know what changed is to look at what came back.

Deliverables

What an engagement produces

Documentation is a deliverable, not an afterthought. On most of these engagements a large part of the value is a defect report precise enough for another team to act on.

Design

  • Every route classified: change frequency, per-visitor variation, crawler requirement
  • Rendering mode per route, with the reason recorded
  • Data source and cache lifetime per route, including what invalidates it
  • Server and client component boundary, and what it means for the bundle
  • Loading and error behaviour per route

Build

  • Routing and layout structure
  • Rendering modes implemented per route
  • Data fetching and caching, with all layers accounted for
  • Error and loading boundaries placed to contain failures
  • Bundle measured and reduced, with the remaining size explained

Verification and handover

  • What each route delivers before JavaScript runs
  • Metadata, structured data, canonicals and redirects checked on the response
  • Bundle size per route, and what the largest contributors are
  • Staleness behaviour verified by changing content and observing propagation
  • Which decisions to revisit as the application grows, and what would prompt it
Under the hood

Architecture and technology

What decides a route's rendering mode

  • How often its content changes, and how quickly a change has to appear
  • Whether it varies per visitor, and whether that can be deferred to the client
  • What a crawler needs to receive before JavaScript runs
  • How expensive its data is to fetch, and how often it is requested
  • Whether it is on the critical path for a first-time visitor

Where Next.js applications go wrong

  • One rendering mode applied to every route
  • Client-side fetching for content that never changes
  • A fetch cache enabled without anyone intending it, producing staleness
  • No error boundary, so one component's failure blanks the route
  • A large client bundle from components that could have been server-rendered
  • Search metadata, canonicals and structured data never verified on the response
Adjacent problems

If this is not quite your problem

These overlap at the edges. Sending you to the right page is more useful than having you work it out.

The architecture is the open question

Choosing and designing a decoupled stack, which is a different engagement.

Headless website development

WordPress is being kept for editors

The full path from CMS through API and frontend to the cache layer.

Headless WordPress development

The application has to be found in search

What a decoupled or client-rendered frontend delivers to a crawler.

JavaScript SEO

The CMS side needs integrating

Connecting a chosen CMS to an existing frontend.

Headless CMS integration
Questions

Frequently asked

How is this different from your headless development page?

One is the architecture and this is the application. Headless website development covers whether to decouple at all, what the stack should be, and how content reaches the frontend — the decisions made before any code exists. This page is about building the Next.js application once that is settled: how each route renders, where its data comes from, how long it is cached, and what happens when it fails. If you are still deciding whether a decoupled architecture is right, that is the other page. If you have one and the application needs engineering, it is this one.

Do you have a Next.js application you can show?

No. Our published work is platform and infrastructure engineering, and none of it is a Next.js application a reader could inspect. What this page describes is the engineering, which is checkable on its own terms: why the rendering mode is a per-route decision, why a fetch cache enabled by accident is the usual cause of staleness, and why an error boundary is a production requirement rather than polish. That is what a reader has to decide about, and it does not depend on a reference.

Should we use the App Router?

It is the current direction and it is not the decision that matters most. What matters is that each route's rendering mode and cache behaviour are chosen deliberately, and the App Router makes those choices more explicit rather than less — server components by default, with caching that has to be considered rather than inherited. Whether a given application should migrate to it depends on how much of it depends on the previous model's behaviour, which is established by looking at the application rather than by a general answer.

Why does our content go stale?

Almost always a cache layer nobody intended to enable. Next.js caches fetches, routes and responses, and the platform underneath caches again, so a stale page is a value held at one of several layers for a reason that was never chosen. Diagnosing it means knowing what each layer's lifetime is and what invalidates it. The fix is usually a decision about which layer should hold the value and for how long, rather than disabling caching — which trades staleness for load.

Will a Next.js frontend help or hurt our search visibility?

It depends entirely on the rendering mode chosen per route, which is why it is a design decision rather than a deployment detail. A statically or server-rendered route delivers complete HTML and is generally at least as good as what it replaced. A client-rendered route delivers a shell and depends on the crawler executing the application — which is a different proposition and not always a worse one, but it is one that has to be verified on the response rather than assumed. Every route's delivered HTML, metadata and structured data are checked as part of the build.

Bring us the problem you have not been able to fix

Describe what is happening rather than what you think the cause is. If we are not the right people for it, we will say so.