Client-Side SDK Initialization Best Practices

This guide is part of the Frontend Integration & Client-Side Rendering series. Getting client-side SDK initialization right determines whether flags are available when the first component renders, whether your React hooks ever see a stale default, and whether the init code itself adds meaningful weight to your critical-path bundle.

This guide covers bootstrap order, initializing from a server-provided snapshot, lazy and deferred init, bundle-size impact, and readiness gating. It does not cover backend targeting rules or server-side evaluation — those live in the Backend Evaluation & Server-Side SDKs series.

Client SDK initialization timeline A timeline showing bootstrap snapshot loading at page start, SDK reaching ready state, then entering live update mode once the streaming connection is established. Bootstrap snapshot server-inlined JSON SDK ready provider initialized Live updates streaming connection t=0 — no network call t≈0 — gates render t+idle — push updates Client SDK initialization phases
A server-inlined snapshot eliminates the network call needed before first render; the provider upgrades to live streaming once idle.

Prerequisites

Core Concept & Architecture

The client SDK follows three distinct phases, and getting the ordering wrong is the most common source of UI flicker during hydration:

  1. Bootstrap — load an initial flag set with zero network round-trips. The server inlines a snapshot into the HTML payload.
  2. Ready — the provider is initialized from that snapshot and reports PROVIDER_READY. Components can now read flags synchronously.
  3. Live — the SDK upgrades to a streaming or polling connection that pushes subsequent changes.

Skipping the bootstrap phase means the SDK must complete a network round-trip before it is ready, which either blocks the render or forces every flag to its default.

Step-by-Step Implementation

Step 1 — Inline a bootstrap snapshot from the server

The server resolves a compact set of flags for the current user and writes the result into the HTML as a <script> tag. The client reads that value before the SDK makes any network request.

// server.ts (Next.js / Express render handler)
import { evaluateBootstrapFlags } from './flag-server';

export async function renderPage(req: Request): Promise<string> {
  // Resolve the flags every above-the-fold component needs
  const snapshot = await evaluateBootstrapFlags({
    targetingKey: req.session.userId,
    'web.nav.new-header': false,
    'web.checkout.express-flow': false,
  });

  // Inline as window.__FLAG_BOOTSTRAP__ — keys use namespace.service.feature
  const snapshotJson = JSON.stringify(snapshot);
  return `<script>window.__FLAG_BOOTSTRAP__ = ${snapshotJson};</script>`;
}

Keep the snapshot small — only the flags needed before first render. Full flag payloads inflate HTML size and defeat the purpose. For payload security guidance see securely passing flags to the browser.

Pitfall: never include targeting-rule logic in the snapshot object. Send resolved variants only — rule trees belong server-side.

Step 2 — Initialize the provider from the snapshot

Wire the provider to consume window.__FLAG_BOOTSTRAP__ before connecting to the remote endpoint. This makes PROVIDER_READY fire synchronously, so the first render sees real values.

// flag-client.ts
import { OpenFeature } from '@openfeature/web-sdk';
import { FlagdWebProvider } from '@openfeature/flagd-web-provider';

let initPromise: Promise<void> | null = null;

export function initFlags(): Promise<void> {
  if (initPromise) return initPromise; // idempotent

  const bootstrap: Record<string, boolean | string | number> =
    (window as any).__FLAG_BOOTSTRAP__ ?? {};

  const provider = new FlagdWebProvider({
    host: 'flags.example.com',
    port: 443,
    tls: true,
    // Hydrate from the inlined snapshot so PROVIDER_READY fires immediately
    cache: { initialValues: bootstrap },
  });

  initPromise = OpenFeature.setProviderAndWait(provider);
  return initPromise;
}

Call initFlags() at the top of your application entry point, before any component tree mounts. Await it in frameworks that support async root setup (Next.js App Router layouts, Nuxt plugins).

Pitfall: calling getClient() before setProviderAndWait resolves returns the no-op provider. Always await init before rendering flag-dependent UI.

Step 3 — Gate rendering on provider readiness

In React, a context provider that awaits initFlags() is the cleanest gate. Components below it can call getBooleanValue synchronously without ever seeing the SDK’s internal default state.

// FlagProvider.tsx
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
import { OpenFeature } from '@openfeature/web-sdk';
import { initFlags } from './flag-client';

const ReadyCtx = createContext(false);

export function FlagProvider({ children }: { children: ReactNode }) {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    initFlags().then(() => setReady(true));
  }, []);

  if (!ready) {
    // Render a skeleton or null while the provider resolves.
    // With a server snapshot this resolves synchronously — the skeleton
    // is visible only on the very first hydration tick.
    return null;
  }

  return <ReadyCtx.Provider value={true}>{children}</ReadyCtx.Provider>;
}

export const useFlagReady = () => useContext(ReadyCtx);

For SSR consistency the server and client must agree on the initial value, so the snapshot approach is doubly important: it ensures the variant the server rendered is what the client hydrates with.

Pitfall: returning null without a skeleton triggers cumulative layout shift. Render a fixed-height placeholder instead.

Step 4 — Subscribe to live updates

Once above-the-fold content has rendered and the page is interactive, the provider upgrades its connection from snapshot-only to a live stream. Subscribe to the PROVIDER_CONFIGURATION_CHANGED event so components react to updates without requiring a page reload.

// live-updates.ts
import { OpenFeature } from '@openfeature/web-sdk';

export function subscribeToFlagUpdates(onUpdate: () => void): () => void {
  const client = OpenFeature.getClient();
  client.addHandler('PROVIDER_CONFIGURATION_CHANGED', onUpdate);
  return () => client.removeHandler('PROVIDER_CONFIGURATION_CHANGED', onUpdate);
}

Use this in a top-level effect to force a re-render of any component that reads flags from context. Combine with React hooks for feature flag state for per-flag subscription granularity.

Pitfall: subscribing before PROVIDER_READY fires is harmless but noisy. Attach the listener inside the initFlags().then(...) callback.

Step 5 — Handle network failure and safe defaults

The streaming connection will fail intermittently. Define safe defaults at the call site rather than relying on the provider’s built-in fallback, so the behavior under failure is explicit and tested.

// Usage in any component
import { OpenFeature } from '@openfeature/web-sdk';

const client = OpenFeature.getClient();

// The second argument is the safe default — returned on any error, timeout, or NOT_READY
const showExpressFlow = client.getBooleanValue(
  'web.checkout.express-flow',
  false // safe default: old flow
);

Keep defaults conservative — the off or degraded variant, not the experimental one. This is especially important for lazy-initialized SDKs where the SDK may not be ready when a below-the-fold component first evaluates a flag.

Verification & Testing

After implementing all steps, confirm the initialization sequence in browser DevTools:

# 1. Open the Network panel and reload — the flags endpoint should NOT appear
#    before DOMContentLoaded (the snapshot serves those values)
# 2. After idle, confirm a single WebSocket or SSE connection to the flags host
# 3. In the Console, run:
window.__FLAG_BOOTSTRAP__
# Expect: { "web.nav.new-header": false, "web.checkout.express-flow": false }
# 4. Toggle a flag in the control plane and confirm the UI updates without reload

For automated verification, write an integration test that mounts your FlagProvider with a seeded snapshot and asserts useFlagReady() returns true before any flag evaluation fires.

Troubleshooting & FAQ

Why does my flag always return its default value on the first render?

The provider is not ready when the component mounts. This usually means initFlags() was not awaited before the component tree rendered, or the bootstrap snapshot was missing from the server response. Check window.__FLAG_BOOTSTRAP__ in the browser console; if it is undefined, the server inlining step failed. If it is populated but flags still default, confirm the provider’s cache.initialValues option is wired to the snapshot object.

How do I avoid a flash of unstyled or wrong-variant content?

Use the server-inlined snapshot so the provider reaches PROVIDER_READY before the first paint. If a snapshot is not feasible, render a neutral skeleton for flag-dependent UI and swap it once the provider is ready. The preventing UI flicker during hydration guide covers the full set of strategies.

Can I initialize the SDK inside a React component rather than at the module level?

Technically yes, but it is a footgun: every component unmount/remount creates a new provider registration, and concurrent renders may call setProviderAndWait more than once. Initialize exactly once at the app entry point using the idempotent pattern in Step 2, then distribute the client via context.

Does the client SDK need the same flags as the server snapshot?

No. The snapshot covers only the flags needed before first render. The provider fetches the complete flag set from the remote once the streaming connection establishes. You do not need to enumerate every flag in the snapshot — only the ones whose absence would cause a visible layout difference on first paint.

Performance & Scale Considerations

The bundle cost of @openfeature/web-sdk is roughly 30–40 KB gzipped for the core client. Named imports and an SDK with a "sideEffects": false declaration in its package.json let your bundler eliminate evaluation branches you don’t use — see minimizing bundle size with tree-shakable SDKs for the specifics. For pages where flags are only needed below the fold, defer the full SDK init to after first paint — see lazy-initializing the client SDK after first paint.