Debarshi Das

Guide

Consent is an analytics execution boundary

A practical Next.js pattern that keeps optional analytics code and collection behind an explicit visitor decision.

By Debarshi Das. Published . Updated .

4 min read

  • Next.js
  • Analytics
  • Privacy
  • Frontend architecture

Many sites build analytics consent as a banner over an unchanged page. The banner asks for permission, but the page has already downloaded a tracker, created identifiers, or started collection. A real consent boundary controls what runs, not only what shows.

Start with a denied default

Until the visitor accepts, the page must act as if the visitor rejected optional analytics. That makes the undecided state safe by design. Public content, navigation, and core features stay available in that state.

"Optional analytics" needs a precise line. On this site, the line is identification. Vercel Analytics counts page views. Speed Insights reports page speed. Neither sets a cookie, and Vercel discards the request hash that tells visits apart after 24 hours, so the counts stay aggregate. Those two run for every visitor. Microsoft Clarity and Google Analytics can connect one visitor's activity across pages and visits, so they sit behind the boundary.

The site moved to this line on 2026-10-17. Before that, all four waited for consent. Most visitors never answered the panel, so the site could not see even how many people read it.

One client component owns the decision. It reads a versioned consent record from browser storage after mount. Before that read finishes, the page renders no analytics runtime. A rejected record keeps the runtime out. An accepted record lets the app request it.

Defer the code as well as the component

A conditional render does not always defer a package. A static import can put an optional analytics SDK in the initial client graph, even when React returns null. The stronger boundary uses a dynamic import, requested only after acceptance.

That distinction matters for privacy and for speed. An undecided visitor should not pay to download, parse, or run optional tooling. A rejected visitor never needs that code.

A browser page holds two anonymous counters from Vercel; Clarity and Google code stays on their servers after a rejection.Hover a part to see its name
The browser page, the consent panel, and the servers the four analytics bundles come from. Vercel's two anonymous counters load for every visitor. Clarity and Google keep their code on their own servers while the visitor is undecided or has rejected. Only acceptance requests it.

Centralize provider behavior

Several analytics providers drift apart when each reads consent on its own. One may start early. Another may fail to stop after withdrawal. The privacy copy may promise behavior that the code does not deliver.

The site therefore has one analytics boundary and one consent record. That boundary activates Microsoft Clarity and Google Analytics. Provider-specific code stays inside the runtime layer. The two anonymous counters sit outside it, in plain view in the same component, so a reader of the code sees both sides of the line in one place.

Withdrawal matters as much. Both gated services accept a denied-consent signal, so both receive it, and the site deletes their cookies. The page then reloads in the rejected state, so optional collection cannot resume.

When the line moves, the consent record's version moves with it. Every visitor who chose under the old terms is asked again.

After withdrawal and reload, two counters still report to Vercel; Clarity and Google sockets and the cookies are empty.Hover a part to see its name
Withdrawing consent. The two providers that can connect visits receive a denied signal, their cookies are cleared, and after the reload they are no longer on the page. Vercel's two anonymous counters keep reporting in every state.

Version the decision

A stored boolean cannot explain which terms a visitor accepted. The record should include at least the decision, a consent version, and a timestamp. When the terms change, the app can treat an older version as undecided and ask for a new choice.

The timestamp is not a server audit log. It is a local record that helps the site apply the visitor's choice every time. The privacy notice should say where the record lives and what it does.

A stored consent record from terms version 3 turns ember in browser storage, and the consent panel rises to ask again.Hover a part to see its name
A consent record is stored in the browser with the version of the terms the visitor accepted. When the terms move to a new version, an older record reads as undecided, and the site asks again.

Keep deployment context explicit

Local development and preview deployments do not need production analytics. Turning collection off there keeps test traffic and preview URLs out of the reports and avoids accidental tracking.

This site enables the analytics configuration only on the Vercel production deployment. Preview pages also send noindex controls. Consent still applies in production.

Test the boundary, not only the banner

Test what a browser can observe:

  • No request from a provider that can connect visits occurs before a decision.
  • The anonymous counters set no cookie.
  • Rejecting consent leaves the site fully usable.
  • Accepting consent loads each approved provider.
  • Withdrawing consent removes optional runtimes and preserves the rejected record.
  • An older consent version causes a new prompt.
  • Public privacy copy matches actual provider behavior.

The banner is the visible part. The execution boundary is the privacy feature.

Choose a style to copy