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.
Prerequisites
@openfeature/web-sdk≥ 1.x added to your project- CSP and security boundaries for client flags
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:
- Bootstrap — load an initial flag set with zero network round-trips. The server inlines a snapshot into the HTML payload.
- Ready — the provider is initialized from that snapshot and reports
PROVIDER_READY. Components can now read flags synchronously. - 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.