Skip to main content

Best Feature Flag Libraries for JavaScript in 2026

·PkgPulse Team
0

TL;DR

OpenFeature + a provider for standards-based flagging; LaunchDarkly for enterprise; Unleash for self-hosted control. OpenFeature (~200K weekly downloads) is the CNCF standard that decouples your code from any vendor. LaunchDarkly (~400K downloads) is the SaaS leader with best-in-class targeting. Unleash (~150K downloads) is the leading open-source option — self-hosted, no vendor lock-in. For new projects, code against OpenFeature so you can swap providers freely.

Key Takeaways

  • LaunchDarkly: ~400K weekly downloads — SaaS leader, 99.99% uptime SLA, streaming updates
  • OpenFeature: ~200K downloads — CNCF standard, vendor-neutral API, provider plugins
  • Unleash: ~150K downloads — open-source, self-hosted, full feature management
  • Flagsmith: ~80K downloads — open-source + SaaS, simpler than Unleash
  • OpenFeature — code once, swap providers; LaunchDarkly, Unleash, Flagsmith all have OpenFeature providers

OpenFeature (The Standard)

// OpenFeature — vendor-neutral feature flag standard
import { OpenFeature } from '@openfeature/server-sdk';

// Register a provider (swap this to change vendors)
import { LaunchDarklyProvider } from '@openfeature/launchdarkly-provider';
// Or: import { UnleashProvider } from '@openfeature/unleash-provider';
// Or: import { FlagsmithProvider } from '@openfeature/flagsmith-provider';
// Or: import { InMemoryProvider } from '@openfeature/server-sdk'; // for tests

await OpenFeature.setProviderAndWait(
  new LaunchDarklyProvider(process.env.LD_SDK_KEY!)
);

const client = OpenFeature.getClient();

// Boolean flag
const newCheckoutEnabled = await client.getBooleanValue(
  'new-checkout-flow',
  false,  // default value if flag missing
);

// String flag (variants)
const checkoutVariant = await client.getStringValue(
  'checkout-variant',
  'control',
);

// Number flag
const maxRetries = await client.getNumberValue('max-retries', 3);

// Object flag (JSON)
const config = await client.getObjectValue('feature-config', {});
// OpenFeature — evaluation context (targeting)
import { OpenFeature, EvaluationContext } from '@openfeature/server-sdk';

const client = OpenFeature.getClient();

// Per-request context for targeting rules
const context: EvaluationContext = {
  targetingKey: user.id,         // Used for % rollouts
  attributes: {
    email: user.email,
    plan: user.plan,             // 'free' | 'pro' | 'enterprise'
    country: user.country,
    betaTester: user.betaTester,
  },
};

// Flag evaluated with targeting context
const enabled = await client.getBooleanValue(
  'pro-dashboard',
  false,
  context
);

// Result: true for pro/enterprise users, false for free
// OpenFeature — React with hooks
import { OpenFeatureProvider, useBooleanFlagValue } from '@openfeature/react-sdk';
import { OpenFeature } from '@openfeature/web-sdk';
import { LaunchDarklyClientProvider } from '@openfeature/launchdarkly-client-provider';

// Setup (client-side SDK)
await OpenFeature.setProviderAndWait(
  new LaunchDarklyClientProvider(
    process.env.NEXT_PUBLIC_LD_CLIENT_ID!,
    { user: { key: userId } }
  )
);

// Provider wraps your app
function App() {
  return (
    <OpenFeatureProvider>
      <Dashboard />
    </OpenFeatureProvider>
  );
}

// Hooks in components
function Dashboard() {
  const newLayout = useBooleanFlagValue('new-dashboard-layout', false);
  const variant = useStringFlagValue('dashboard-variant', 'v1');

  return newLayout ? <NewDashboard variant={variant} /> : <OldDashboard />;
}
// OpenFeature — in-memory provider for tests (no external deps)
import { InMemoryProvider, OpenFeature } from '@openfeature/server-sdk';

const flagConfig = {
  'new-checkout-flow': {
    defaultVariant: 'on',
    variants: { on: true, off: false },
  },
  'checkout-variant': {
    defaultVariant: 'test-variant',
    variants: { control: 'control', 'test-variant': 'B' },
  },
};

OpenFeature.setProvider(new InMemoryProvider(flagConfig));
// Now tests run without network calls or SDK keys

LaunchDarkly (Enterprise SaaS)

// LaunchDarkly — server-side SDK
import * as ld from '@launchdarkly/node-server-sdk';

const client = ld.init(process.env.LD_SDK_KEY!, {
  // Optional: polling fallback if streaming fails
  stream: true,
  // Optional: private attributes (not sent to LD analytics)
  allAttributesPrivate: false,
  privateAttributes: ['email'],
});

await client.waitForInitialization({ timeout: 5 });

// Evaluate with user context
const context: ld.LDContext = {
  kind: 'user',
  key: user.id,
  name: user.name,
  email: user.email,
  custom: {
    plan: user.plan,
    country: user.country,
  },
};

// Boolean flag
const enabled = await client.variation('dark-mode', context, false);

// String flag
const theme = await client.variation('ui-theme', context, 'default');

// Flag with detailed reason (for debugging)
const detail = await client.variationDetail('new-feature', context, false);
console.log(detail.reason); // { kind: 'RULE_MATCH', ruleIndex: 0 }
// LaunchDarkly — multi-context (user + organization)
const context: ld.LDContext = {
  kind: 'multi',
  user: {
    kind: 'user',
    key: user.id,
    email: user.email,
  },
  organization: {
    kind: 'organization',
    key: org.id,
    name: org.name,
    plan: org.plan,           // Target by org plan
  },
};

// Flag can now target by user OR org attributes
const enabled = await client.variation('org-feature', context, false);

Best for: Enterprise teams needing 99.99% SLA, advanced targeting, A/B testing integration, audit logs.


Unleash (Self-Hosted Open Source)

// Unleash — self-hosted Node.js SDK
import { initialize, isEnabled } from 'unleash-client';

initialize({
  url: 'https://unleash.yourcompany.com/api',
  appName: 'your-app',
  customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN! },
});

// Simple boolean check
if (isEnabled('new-checkout', { userId: user.id })) {
  // Use new checkout
}
// Unleash — full client with context
import Unleash from 'unleash-client';

const unleash = new Unleash({
  url: 'https://unleash.yourcompany.com/api',
  appName: 'pkgpulse',
  customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN! },
  // Metrics reported to Unleash dashboard
  metricsInterval: 60_000,
  // Cache flags locally for 15s
  refreshInterval: 15_000,
});

await unleash.start();

const context = {
  userId: user.id,
  sessionId: req.sessionId,
  remoteAddress: req.ip,
  properties: {
    plan: user.plan,
    country: user.country,
  },
};

// Strategy: gradual rollout to 20% of users
const enabled = unleash.isEnabled('beta-dashboard', context);

// Variant (A/B)
const variant = unleash.getVariant('checkout-variant', context);
// variant.name = 'control' | 'A' | 'B'
// variant.payload.value = custom JSON

// Cleanup
process.on('SIGTERM', () => unleash.destroy());
// Unleash — Next.js integration
// lib/unleash.ts (server-side only)
import { createUnleash } from 'unleash-client';

let unleashInstance: Unleash;

export function getUnleash() {
  if (!unleashInstance) {
    unleashInstance = createUnleash({
      url: process.env.UNLEASH_URL!,
      appName: 'pkgpulse',
      customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN! },
    });
  }
  return unleashInstance;
}

// In API route or server component
export async function GET(req: Request) {
  const unleash = getUnleash();
  const enabled = unleash.isEnabled('new-api', {
    userId: getUserId(req),
  });

  return Response.json({ enabled });
}

Best for: Teams that need full control over flag data, no vendor lock-in, or have compliance requirements preventing SaaS.


Flagsmith (Simpler Self-Hosted)

// Flagsmith — simpler API, cloud + self-hosted
import Flagsmith from 'flagsmith-nodejs';

const flagsmith = new Flagsmith({
  environmentKey: process.env.FLAGSMITH_ENV_KEY!,
  // self-hosted: apiUrl: 'https://flagsmith.yourcompany.com/api/v1'
});

// Get all flags for a user
const flags = await flagsmith.getIdentityFlags(user.id, {
  traits: {
    plan: user.plan,
    email: user.email,
  },
});

// Check feature
const enabled = flags.isFeatureEnabled('dark-mode');

// Get remote config value
const theme = flags.getFeatureValue('theme-config');
// Returns the value set in the Flagsmith UI (string, number, JSON)

Comparison Table

ToolTypePricingTargetingSDK QualitySelf-Hosted
OpenFeatureStandard (no backend)FreeVia provider✅ ExcellentN/A
LaunchDarklySaaS$10+/seat✅ Advanced✅ Excellent
UnleashOSS + SaaSFree (self-hosted)✅ Good✅ Good
FlagsmithOSS + SaaSFree (self-hosted)✅ Basic✅ Good
GrowthBookOSS + SaaSFree (self-hosted)✅ A/B focused✅ Good

When to Choose

ScenarioPick
Any new projectOpenFeature SDK + any provider
Enterprise, needs SLA + advanced targetingLaunchDarkly
Full data ownership, complianceUnleash (self-hosted)
Simple flags, wants self-hosted optionFlagsmith
A/B testing + feature flags combinedGrowthBook
Testing in CI without external depsOpenFeature InMemoryProvider
Migrating vendors without code changesOpenFeature

The Case for OpenFeature Abstraction

The most important architectural decision in feature flagging is whether to code against a vendor-specific SDK or a standards-based abstraction. Teams that coded directly against LaunchDarkly's SDK in 2019 found themselves locked to LaunchDarkly's pricing, uptime, and feature set. Migrating to Unleash or Flagsmith required touching every flag evaluation call in the codebase — often hundreds of sites in a mature application. OpenFeature exists specifically to prevent this outcome.

When you code against OpenFeature's API, the flag evaluation code is client.getBooleanValue('flag-name', false, context) regardless of which provider is registered. Switching from LaunchDarkly to Unleash is a one-file change in your provider registration. This vendor portability has real economic value: you can negotiate with providers, migrate when pricing changes, or adopt a self-hosted solution without a codebase migration. The OpenFeature SDK itself is stable, CNCF-backed, and unlikely to change its core API in breaking ways.

Gradual Rollout Strategies and Statistical Significance

Feature flags are most valuable not as simple on/off switches but as gradual rollout mechanisms. A 5% rollout of a new checkout flow lets you measure conversion rate impact before committing to 100% of users. A 10% rollout of a new recommendation algorithm lets you A/B test performance metrics. All four tools support percentage-based rollouts, but they implement them differently in ways that affect the user experience.

The quality of a percentage rollout depends on its stability: the same user should see the same variant on every request, not randomly get the new or old experience. This requires a deterministic bucketing algorithm based on the user's identifier — typically a hash of the targetingKey modulo 100. LaunchDarkly and Unleash both implement stable bucketing correctly. OpenFeature delegates this to the provider, so the bucketing quality depends on which provider you use. Flagsmith's gradual rollout uses a similar hash-based approach.

For A/B testing that requires statistical significance calculations, feature flag SDKs alone are not sufficient — you need a results analysis layer that tracks conversion events and calculates whether the difference between variants is statistically significant. GrowthBook is the only tool in this comparison that combines feature flagging with built-in experiment analysis, making it the better choice for teams doing serious product experimentation rather than simple staged rollouts.

Flag Evaluation Latency and Caching

Feature flag evaluation happens in the hot path of request handling — it can occur dozens of times per request in complex applications. The latency of flag evaluation matters. LaunchDarkly's server-side SDK uses a streaming connection to receive real-time flag updates and evaluates flags from a local in-memory cache — no network round trip per evaluation. Evaluation latency is sub-millisecond for cached flags.

Unleash's client uses a polling model by default: every 15 seconds (configurable), the client fetches the current flag state from the Unleash server and caches it locally. Between polls, evaluation is local and fast. The polling interval creates a propagation delay: a flag change in the Unleash UI takes up to 15 seconds to reach all running instances. LaunchDarkly's streaming model propagates changes in under a second. For most feature flag use cases, 15 seconds is acceptable. For emergency kill switches where you need instant propagation — killing a broken feature immediately — LaunchDarkly's streaming model has a meaningful advantage.

OpenFeature's latency depends entirely on the provider. The LaunchDarkly provider uses LaunchDarkly's streaming model. The Unleash provider uses Unleash's polling model. For maximum control over evaluation latency, teams can build a custom provider that serves flags from an in-process cache refreshed by any mechanism they choose.

Multi-Environment Flag Management

Production feature flag systems need to support multiple environments (development, staging, production) with different flag states in each. A flag that is 100% on in development for testing purposes should not accidentally be 100% on in production. All four tools support environment separation, but the governance model differs.

LaunchDarkly has the most sophisticated environment management: each environment has its own SDK key, flags are configured per-environment, and targeting rules can differ between environments. Approvals for production flag changes (requiring a second person to approve changes) are available in higher tiers. Unleash supports multiple environments with per-environment activation and rollout percentages, with the environment management being a core (non-paid) feature. Flagsmith's environment model is similar to Unleash.

For teams with compliance requirements — SOC 2, PCI-DSS — the audit trail of who changed which flag when, and who approved the change, matters as much as the flag functionality. LaunchDarkly's audit log and approval workflow features are designed for these requirements. Self-hosted Unleash can generate audit logs but requires connecting those logs to your existing SIEM or audit infrastructure.

Testing with Feature Flags

Feature flags introduce conditional code paths that must be tested. A flag with two variants (on/off) doubles the number of state combinations in any component that uses it. With ten flags, you have 1,024 theoretical state combinations. Practical test strategies don't test every combination, but they must test the critical paths.

OpenFeature's InMemoryProvider is the cleanest solution for unit testing: you configure which flags are on or off in your test setup, and every flag evaluation in the test returns the configured value without any network calls, SDK initialization, or external dependencies. This makes unit tests fast, deterministic, and independently runnable.

For integration tests, the right approach is to set up a test environment in your flag management system with flags configured for each test scenario, or to use provider-specific test utilities. LaunchDarkly's SDK provides a LDTestDataSource that allows flag overrides in tests. Unleash provides a mock client for testing. Flagsmith has no official testing utilities, requiring teams to mock the SDK directly.

The best practice is to define flag values in test utilities as named constants rather than hardcoded strings, and to centralize flag names in a type-safe constants file. This prevents typos in flag names from causing silent failures where a test runs against a flag that does not exist (and thus gets the default value, which may mask test failures). End-to-end tests should always run against a dedicated test environment with flags in known states, never against production flag configurations — a production flag change should not break an end-to-end test run.

Compare feature flag package health on PkgPulse.

See also: AVA vs Jest and AVA npm stats, 20 Fastest-Growing npm Packages in 2026 (Data-Backed).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.