🔧 Lightweight A/B testing SDK with multiple traffic splitting strategies and optional React integration, built with robuild, only 2.2 kb
简体中文 | English
Core Features:
- Zero Dependencies Core: Pure JavaScript implementation, works standalone
- Optional React Integration: Provides Hooks and Context API
- Multiple Splitting Strategies: Random, CRC32, custom functions
- Persistent Storage: localStorage-based result caching with custom storage support
- Flexible Configuration: Supports Baidu Analytics or fully custom
- Incremental Updates: Smart config change detection and re-splitting
- Debug Friendly: URL parameter force hit, controllable logging
- High Test Coverage: 100% core logic coverage, 94%+ overall coverage
npm install abtest-kit
# or
pnpm add abtest-kit
# or
yarn add abtest-kitOptional Dependencies:
- React 18+ (only required when using React integration)
The SDK provides two separate entry points to optimize bundle size:
// Method 1: Core API only (no React dependency, smaller bundle)
import { initGlobalABTest, getGlobalABTestValue } from 'abtest-kit';
// Method 2: React integration (includes React components and hooks)
import { ABTestProvider, useABTest, useABTestValue } from 'abtest-kit/react';| Entry Point | Bundle Size | React Required | Use Case |
|---|---|---|---|
abtest-kit |
~1.8 KB | No | Vanilla JS, Vue, Angular, etc. |
abtest-kit/react |
~2.4 KB | Yes | React applications |
Suitable for any JavaScript project, perform traffic splitting on page load:
import { initGlobalABTest, getGlobalABTestValue } from 'abtest-kit';
// Define splitting configuration
const config = {
newFeature: {
key: 'new_feature',
groups: {
0: 50, // Control group 50%
1: 50 // Experiment group 50%
}
}
};
// Initialize splitting (results are automatically cached to localStorage)
const result = initGlobalABTest(config);
// Get splitting value anywhere
const featureValue = getGlobalABTestValue('newFeature');
if (featureValue === 1) {
// Show new feature
} else {
// Show old feature
}Suitable for React applications, provides reactive splitting state:
import { ABTestProvider, useABTestValue } from 'abtest-kit/react';
const abTestConfig = {
featureA: {
key: 'feature_a',
value: -1,
groups: { 0: 50, 1: 50 },
strategy: 'random'
}
};
function App() {
return (
<ABTestProvider abTestConfig={abTestConfig}>
<YourComponent />
</ABTestProvider>
);
}
function YourComponent() {
const featureValue = useABTestValue('featureA');
return (
<div>
{featureValue === 1 ? 'New Feature' : 'Old Feature'}
</div>
);
}Initialize global traffic splitting, results are cached to localStorage.
const result = initGlobalABTest(
{
test1: {
key: 'test1',
groups: { 0: 50, 1: 50 }
}
},
{
strategy: 'random', // 'random' | 'crc32' | custom function
userId: 'user123', // required for crc32 strategy
storageKey: '__abtest__' // custom storage key
}
);Get the splitting value for a specific test.
const value = getGlobalABTestValue('test1'); // Returns 0 or 1 or -1 (not initialized)Get the statistics string for all splitting results.
const userstat = getGlobalABTestUserstat(); // "test_1-0;test_2-1"Clear cache and re-split.
const newResult = resetGlobalABTest(config);Note: React API should be imported from
abtest-kit/react
React context provider.
import { ABTestProvider } from 'abtest-kit/react';
<ABTestProvider
abTestConfig={config}
options={{ userId: 'user123' }}
injectScript={() => {
// Optional: inject Baidu Analytics script
}}
>
<App />
</ABTestProvider>Get the complete AB test context.
import { useABTest } from 'abtest-kit/react';
const { abTestConfig, pending, userstat } = useABTest();Get the value for a specific test.
import { useABTestValue } from 'abtest-kit/react';
const value = useABTestValue('test1');Completely random splitting, randomly assigned on each initialization.
initGlobalABTest(config, { strategy: 'random' });Deterministic splitting based on user ID, same user always assigned to same group.
initGlobalABTest(config, {
strategy: 'crc32',
userId: 'user_12345'
});Why CRC32?
In first-screen performance optimization scenarios, we cannot wait for splitting results before determining the user path. CRC32 provides a fast and stable splitting solution:
- Problem with traditional approach:
Math.random()changes on every refresh, making it impossible to determine stable experiment group assignment, causing data alignment issues in performance analysis. - CRC32 benefits:
- Stable and reproducible experiment grouping
- No waiting for splitting results during first-screen loading
- Consistent numerator/denominator metrics for analytics
- Can be reproduced in SQL:
crc32(userId) % 100
Pass a custom function to implement specific splitting logic.
// Global custom strategy
initGlobalABTest(config, {
strategy: (groups) => {
// Time-based splitting
const hour = new Date().getHours();
return hour % 2 === 0 ? 0 : 1;
}
});
// Per-experiment custom strategy
const config = {
test1: {
key: 'test1',
groups: { 0: 50, 1: 50 },
strategy: (groups) => {
// Only applies to this experiment
return Math.random() > 0.7 ? 1 : 0;
}
}
};Integration with Baidu Analytics A/B testing platform (requires React).
import { ABTestProvider } from 'abtest-kit/react';
<ABTestProvider
abTestConfig={{
test1: {
key: 'test1',
value: -1,
strategy: 'baiduTongji'
}
}}
injectScript={() => {
const script = document.createElement('script');
script.src = '//hm.baidu.com/hm.js?YOUR_SITE_ID';
document.head.appendChild(script);
}}
>
<App />
</ABTestProvider>- Minimal Dependencies: Core functionality has no framework dependencies
- Progressive Enhancement: React integration as optional extension
- Flexible & Extensible: Support custom strategies and cache implementations
- Performance First: Initialization <5ms, memory <1KB
- Developer Experience: Complete TypeScript type support
+-------------------------------------------------------------+
| Application Layer |
| +---------------------+ +-----------------------------+ |
| | React Apps | | Non-React Apps | |
| | (Vue/Angular/JS) | | (Early page load/Pure JS) | |
| +----------+----------+ +--------------+--------------+ |
+-------------|-----------------------------|------------------+
| |
+-------------v-----------------------------v------------------+
| SDK Entry Points |
| +---------------------+ +-----------------------------+ |
| | abtest-kit/react | | abtest-kit | |
| | (~2.4 KB) | | (~1.8 KB) | |
| | - ABTestProvider | | - initGlobalABTest | |
| | - useABTest | | - getGlobalABTestValue | |
| | - useABTestValue | | - setGlobalCache | |
| +----------+----------+ +--------------+--------------+ |
+-------------|-----------------------------|------------------+
| |
+-------------+---------------+
|
+---------------------------v---------------------------------+
| Core Layer |
| +--------------+ +--------------+ +----------------------+ |
| | Strategies | | Storage | | Utilities | |
| | - Random | | - CacheStorage| | - Logger | |
| | - CRC32 | | - localStorage| | - forceHitTestFlag | |
| | - Custom | | - Custom impl | | - getConfigHash | |
| | - BaiduTongji| +--------------+ +----------------------+ |
| +--------------+ |
+-------------------------------------------------------------+
Input: new configMap
|
Read from cache: storedResult, storedConfigHashes
|
For each key in configMap:
|
+-- key exists in cache AND configHash unchanged?
| -> Keep original value
|
+-- key exists in cache BUT configHash changed?
| -> Re-split
|
+-- key not in cache?
-> New split
|
Save new results (only keys in current configMap)
|
Return splitting results
{
"result": {
"experimentA": 0,
"experimentB": 1
},
"configHashes": {
"experimentA": "0:50|1:50",
"experimentB": "0:30|1:70"
}
}By default, splitting results are stored in localStorage. You can customize the cache implementation using setGlobalCache.
interface CacheStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}import { setGlobalCache, initGlobalABTest } from 'abtest-kit';
setGlobalCache({
getItem: (key) => sessionStorage.getItem(key),
setItem: (key, value) => sessionStorage.setItem(key, value),
removeItem: (key) => sessionStorage.removeItem(key)
});
initGlobalABTest(config);import { setGlobalCache } from 'abtest-kit';
setGlobalCache({
getItem: (key) => getCookie(key),
setItem: (key, value) => setCookie(key, value, { expires: 365 }),
removeItem: (key) => deleteCookie(key)
});import { setGlobalCache } from 'abtest-kit';
const cache = new Map();
async function initCache() {
const data = await loadFromIndexedDB();
data.forEach((value, key) => cache.set(key, value));
}
setGlobalCache({
getItem: (key) => cache.get(key) ?? null,
setItem: (key, value) => {
cache.set(key, value);
saveToIndexedDB(key, value);
},
removeItem: (key) => {
cache.delete(key);
removeFromIndexedDB(key);
}
});import { resetGlobalCache } from 'abtest-kit';
resetGlobalCache();Force Hit Mode:
https://example.com?forceHitTestFlag=experiment_a-1;experiment_b-0
- Force specify splitting results via URL parameters
- Convenient for development and testing
Log Control:
import { logger, LogLevel } from 'abtest-kit';
logger.setLevel(LogLevel.DEBUG); // Enable detailed logs
logger.setLevel(LogLevel.ERROR); // Show errors only- React API's default splitting strategy is based on Baidu Analytics, ensure Baidu Analytics experiment splitting is properly configured before using the SDK
- Initialization is asynchronous, consider the
pendingstate when usinguseABTestValue - Global splitting uses localStorage by default, ensure browser supports localStorage, or use
setGlobalCacheto customize storage - Global splitting results are permanently retained once saved, unless actively calling
resetGlobalABTest()orclearGlobalABTestCache() - Configuration changes (including traffic ratio adjustments) will trigger re-splitting, modify configuration with caution
- Custom cache must be set before calling
initGlobalABTest
- Centralize A/B test configuration management
- Use TypeScript to define configuration types
- Properly use forced test mode for development debugging
- Call global splitting early in page load to ensure consistency
- Use different storageKeys for different tests to avoid conflicts
| Metric | Value |
|---|---|
| Initialization time | <5ms |
| Memory usage | <1KB |
| Core bundle size | ~1.8KB (gzip) |
| React bundle size | ~2.4KB (gzip) |
- Browsers: Modern browsers supporting ES6+
- React: 18.0+
- Node.js: Requires localStorage polyfill for SSR
