Feature Flags on Static Sites

Feature flags separate deploying code from releasing features. Code for a new search interface, a redesigned pricing table or an interactive tutorial ships to production switched off, and the team turns it on when they are ready — for everyone, for internal users first, or for a percentage of readers. If something goes wrong, the feature is switched off again without a deploy or a rollback of unrelated changes.

On a server-rendered application, flags are evaluated per request. A static site has no request-time server by default, which is why teams often assume flags do not apply. They do, in three forms: build-time flags, flags evaluated at the edge, and flags evaluated in the browser. This guide covers when to use each, how to implement them without flicker or layout shift, and how to keep flags from turning into permanent clutter. It is part of Rollbacks and Deploy Safety for Static Sites.

Prerequisites

  • A static site with a deployment pipeline you trust — see Rolling Back a Bad Static Deploy in Under a Minute.
  • For edge flags: a host with edge functions (Cloudflare Workers, Netlify Edge Functions, Vercel Middleware).
  • Optional: a flag service such as LaunchDarkly, Flagsmith, GrowthBook or Unleash, or a simple JSON file in a key-value store.

Three Places to Evaluate a Flag

Build-time, edge and client feature flags compared Build-time flags are evaluated during the build, need a rebuild to change, have no runtime cost and no flicker. Edge flags are evaluated by a CDN function per request, change instantly, can target users, and serve the right HTML without flicker. Client flags are evaluated in the browser, change instantly and target users, but risk flicker and layout shift and add JavaScript. Where the flag is evaluated decides its trade-offs Build time toggle → rebuild (minutes) same for every reader no runtime cost no flicker off variant not shipped release toggles Edge toggle → instant per user, cookie, country no flicker both variants built small edge cost per request gradual rollouts, kill switches Client toggle → instant per user in the browser flicker, layout shift risk extra JavaScript both variants shipped below-the-fold widgets
Prefer the earliest point that meets the need; move later only when you need instant or per-user toggles.

Build-Time Flags

The simplest flag is a value read during the build. The generator decides what to output, and the HTML contains only the enabled variant:

// src/config/flags.js
export const flags = {
  newSearch: process.env.FLAG_NEW_SEARCH === 'true',
  pricingV2: process.env.FLAG_PRICING_V2 === 'true',
};
---
import { flags } from '../config/flags';
import SearchV1 from '../components/SearchV1.astro';
import SearchV2 from '../components/SearchV2.astro';
---
{flags.newSearch ? <SearchV2 /> : <SearchV1 />}

Toggle by changing the environment variable in the host's settings and triggering a rebuild. Hugo reads environment variables through getenv or site parameters in an environment-specific config file; Eleventy reads them in a global data file.

Build-time flags are ideal for release toggles: finish a feature on main behind a flag, ship it dark with every deploy, and flip it on for launch. Because the off variant is never in the HTML, unreleased pages and copy stay private, which client flags cannot guarantee. The limitation is speed — turning a feature off means a rebuild, typically a few minutes.

Edge Flags

For instant toggles and per-user targeting without flicker, evaluate flags at the edge. Build both variants into the site, and let an edge function choose which one to serve.

Two patterns work well:

  1. Route to a variant path. Build /pricing/ and /pricing-v2/; the edge function rewrites /pricing/ to /pricing-v2/ when the flag is on for this request. The reader sees /pricing/ in the address bar.
  2. Rewrite HTML in flight. Build one page with both variants marked, such as <div data-flag="newSearch" data-variant="on">, and use an HTML rewriter at the edge to remove the variant that is off.
// Cloudflare Worker: variant routing with a KV-stored flag
export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    if (url.pathname === '/pricing/') {
      const rollout = Number((await env.FLAGS.get('pricingV2')) ?? 0); // 0..100
      const bucket = bucketFor(request.headers.get('Cookie'));          // stable 0..99 per visitor
      if (bucket < rollout) url.pathname = '/pricing-v2/';
    }
    return env.ASSETS.fetch(new Request(url, request));
  },
};

Changing the KV value takes effect within seconds, with no rebuild. The visitor's bucket comes from a cookie set on first visit, so each reader consistently sees the same variant. The HTML served is complete and correct for that reader, so there is no flicker and no layout shift. The same approach powers the traffic split in Canary Releases for Static Sites.

When HTML varies per visitor, make sure the CDN does not cache one visitor's variant for another: add Vary: Cookie, cache by variant path rather than original path, or skip caching flagged HTML.

Client Flags

Client-side flags are evaluated by a JavaScript SDK after the page loads. They are the most flexible and the most dangerous for performance: a component that appears, disappears or changes after first paint causes layout shift and a visible flicker.

Use them for changes that are below the fold, inside interactive widgets, or not visual at all — an analytics event, a different API endpoint for search. When a client flag must affect visible content:

  • Render the default in the static HTML and switch only when the flag is on, so most readers see no change.
  • Reserve space for anything the flag might add, using fixed dimensions.
  • Evaluate before paint only for tiny changes, with a small inline script and flags cached in localStorage from the previous visit.
  • Never block rendering waiting for a flag service to respond.
Layout shift from the same banner flagged three ways A promotional banner above the article controlled by a flag. Evaluated on the client after load, it caused CLS p75 of 0.14. Evaluated on the client with reserved space, CLS was 0.03 but the space was empty for readers without the flag. Evaluated at the edge, CLS was 0.01 and the HTML was correct for every reader. CLS p75 for a flagged banner above the article 0.1 good threshold 0.14 client, after load 0.03 client, space reserved 0.01 edge-evaluated reserved space avoided the shift but left an empty band for readers without the flag
Anything visible above the fold should be flagged at build time or at the edge.

Keeping Flags Under Control

Flags are temporary by design and permanent by default. Each one adds a code path to test, and old flags make templates hard to read. A few habits keep the number down:

  • Register every flag in one file or service with an owner, a purpose and an expiry date.
  • Fail CI on expired flags. A script that reads the registry and fails when a flag is past its date forces a decision: remove it or extend it deliberately.
  • Remove flags after launch. Once a feature has been on for everyone for a release or two, delete the flag and the old code path in one pull request.
  • Test both states. Run the build and key checks with each release flag on and off, at least before the launch, so flipping it is not the first time the combination is exercised.
Lifecycle of a release flag A flag is created with an owner and expiry, code ships dark behind it, the feature is rolled out, the flag stays fully on for a release or two, and then the flag and the old code path are removed. A CI check fails if the flag passes its expiry date before removal. Every flag has an end create owner + expiry ship dark flag off roll out 10 → 50 → 100% fully on 1–2 releases remove flag + old path CI fails if the expiry date passes before the "remove" step kill switches for risky integrations are the exception: long-lived by design, still registered
The removal step is part of the feature, not an optional clean-up.

Measured Impact

A SaaS documentation and marketing site built with Astro on Cloudflare adopted build-time flags for release toggles and edge flags for gradual rollouts. A redesigned pricing page was rolled out through 10%, 50% and 100% over a week with a KV-stored rollout value, while conversion was compared between groups; a checkout link bug found at 10% was fixed before wider rollout. A client-flagged banner that had caused a CLS regression was moved to the edge, bringing the homepage's CLS p75 from 0.14 back to 0.01. A CI check on flag expiry kept the registry below ten active flags.

Pitfalls & Rollback

  • Unreleased content in client bundles. Client flags ship both variants; use build-time flags for anything confidential.
  • Cached variants. Per-visitor HTML cached without a Vary or variant key leaks one reader's variant to others.
  • Flag service outages. Default to the safe variant when the flag source is unreachable, at the edge and on the client.
  • Nested flags. Features that depend on other flags multiply test combinations; keep flags independent.
  • Rollback: set the flag off; for build-time flags, rebuild or redeploy the previous build.

Conclusion

Static sites can use feature flags as effectively as server-rendered ones by choosing where to evaluate them. Build-time flags suit release toggles and keep unreleased content private; edge flags give instant, per-reader toggles and gradual rollouts without flicker; client flags suit below-the-fold and non-visual changes. Register every flag with an owner and expiry, test both states, and remove flags once the feature has shipped.

FAQ

Can a static site use feature flags?

Yes, in three ways. Build-time flags decide what is generated and need a rebuild to change. Edge flags are evaluated by a CDN function per request and can switch features instantly. Client flags are evaluated in JavaScript in the browser, which is flexible but can cause flicker and layout shift.

How do I avoid flicker with client-side feature flags?

Render the default state in the static HTML and only enhance it when the flag is on, reserve space for any element the flag might show, and evaluate flags before first paint only for small, above-the-fold changes. For larger changes, evaluate at the edge and serve the right HTML directly.

When should I use build-time flags?

For features tied to a release, such as a new section or navigation change, where a rebuild on toggle is acceptable. Build-time flags add no runtime cost and no flicker, and the output contains only the enabled variant, which keeps unreleased content out of the public HTML.

How do I stop feature flags from piling up?

Give every flag an owner and an expiry date when it is created, list flags in one configuration file or service, and remove the flag and the dead code path once a feature has been fully on for a release or two. A CI check can fail when a flag is past its expiry.