Ship 60-90% less JavaScript to devices that can't handle it.
Getting Started · Live Demo · Why Adaptive? · API Reference · CLI · STB/CTV Guide
- Zero code changes to start — install the Vite plugin, get a full bundle analysis report on every build
- One-line adaptive boundaries — wrap heavy components, ship lightweight alternatives to low-end devices
- Build-time intelligence — chunk isolation guarantees no high-tier code leaks into low-tier bundles
- < 50ms detection — hardware scoring with fast-path optimization, cached across sessions
- ~3KB runtime — zero-dependency core with framework adapters for React, Vue, and Svelte
- Meta-framework support — first-class Next.js and Nuxt integrations
- STB/CTV ready — the only adaptive loading tool with set-top box and connected TV support
- 100% local — zero telemetry, zero network calls, GDPR-compatible by design
Modern web apps ship the same JavaScript to every device. A flagship phone with 12GB RAM gets the same 1.2MB bundle as a budget phone with 2GB RAM. On STBs and CTV devices, the problem is even worse.
No production-grade tooling exists for adaptive loading. Google Chrome Labs validated the pattern in 2019 but abandoned it. Adaptive makes it practical.
pnpm add -D @adaptive-bundle/vite-plugin
pnpm add @adaptive-bundle/core
# Pick your framework adapter:
pnpm add @adaptive-bundle/react # React 18+
pnpm add @adaptive-bundle/vue # Vue 3.3+
pnpm add @adaptive-bundle/svelte # Svelte 4+
pnpm add @adaptive-bundle/next # Next.js 13+
pnpm add @adaptive-bundle/nuxt # Nuxt 3+// vite.config.ts
import { adaptive } from '@adaptive-bundle/vite-plugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [adaptive()],
});Build your app -- the plugin outputs a bundle analysis report with heavy dependencies, potential savings, and suggested adaptive boundaries ranked by impact.
A multi-page React dashboard showcasing every adaptive pattern — animated metrics (framer-motion), interactive 3D scene (Three.js), rich markdown editor, canvas charts — all with lightweight low-tier alternatives.
Try the live demo — add ?adaptive_tier=low to any URL to see the low-tier experience.
# Or run locally:
cd fixtures/demo-app
pnpm dev # http://localhost:5173See the Demo App README for architecture details and what each page demonstrates.
Exclude a heavy component on low-tier devices entirely:
import { adaptive } from '@adaptive-bundle/react';
const MapView = adaptive({
component: () => import('./MapboxMap'),
lowFallback: <img src="/static-map.png" alt="Map" />,
layout: { width: '100%', aspectRatio: '16/9' },
});
<MapView center={[40, -74]} zoom={12} />;Ship different implementations per tier:
const Editor = adaptive({
high: () => import('./RichEditor'),
low: () => import('./BasicEditor'),
name: 'editor',
});Opt-in per boundary by adding a medium variant:
const Chart = adaptive({
high: () => import('./AnimatedChart'),
medium: () => import('./StaticChart'),
low: () => import('./ChartTable'),
thresholds: { high: 0.65, low: 0.35 },
});Score >= 0.65 loads high, < 0.35 loads low, between loads medium.
Conditional rendering within JSX:
import { Adaptive } from '@adaptive-bundle/react';
function Dashboard() {
return (
<div>
<Adaptive.High>
<AnimatedChart data={data} />
</Adaptive.High>
<Adaptive.Low>
<StaticTable data={data} />
</Adaptive.Low>
</div>
);
}<script setup>
import { adaptive } from '@adaptive-bundle/vue';
const MapView = adaptive({
high: () => import('./MapboxMap.vue'),
low: () => import('./StaticMap.vue'),
});
</script>
<template>
<component :is="MapView" :center="[40, -3]" :zoom="12" />
</template><script>
import { adaptive } from '@adaptive-bundle/svelte';
const MapView = adaptive({
high: () => import('./MapboxMap.svelte'),
low: () => import('./StaticMap.svelte'),
});
</script>
<svelte:component this={$MapView} center={[40, -3]} zoom={12} />Control when adaptive boundaries begin loading their imports:
| Strategy | Behavior | Use case |
|---|---|---|
viewport (default) |
Load on first render | General purpose |
eager |
Preload import immediately at definition time | Critical above-the-fold content |
lazy |
Defer import until element enters viewport | Heavy below-the-fold content |
// React — preload critical metrics immediately
const Metrics = adaptive({
high: () => import('./AnimatedMetrics'),
low: () => import('./StaticMetrics'),
loading: 'eager',
});
// React — defer heavy 3D scene until scrolled into view
const Scene = adaptive({
high: () => import('./ThreeScene'),
low: () => import('./StaticScene'),
loading: 'lazy',
});<!-- Vue -->
<script setup>
const Chart = adaptive({
high: () => import('./AnimatedChart.vue'),
low: () => import('./StaticChart.vue'),
loading: 'lazy',
});
</script><!-- Svelte — lazy with viewport action -->
<script>
import { adaptive, viewportAction } from '@adaptive-bundle/svelte';
const Scene = adaptive({
high: () => import('./ThreeScene.svelte'),
low: () => import('./StaticScene.svelte'),
loading: 'lazy',
});
</script>
<div use:viewportAction={() => Scene.load()}>
{#if $Scene}
<svelte:component this={$Scene} />
{/if}
</div>SSR safe: when IntersectionObserver is unavailable, lazy falls back to default behavior.
import { useAdaptive, useTier, useDeviceProfile, useNetworkAware } from '@adaptive-bundle/react';
function MyComponent() {
const { tier, shouldDefer, profile } = useAdaptive();
const tier = useTier(); // 'high' | 'low' | 'medium'
const profile = useDeviceProfile(); // full DeviceProfile
const { shouldDefer, effectiveType } = useNetworkAware();
}Wrap your app in AdaptiveProvider to cache the profile across hooks and prevent re-detection on every hook call:
import { AdaptiveProvider } from '@adaptive-bundle/react';
<AdaptiveProvider>
<App />
</AdaptiveProvider>;import { useAdaptive, useTier, useDeviceProfile, useNetworkAware } from '@adaptive-bundle/vue';
const { tier, shouldDefer, profile } = useAdaptive();
const tier = useTier();
const profile = useDeviceProfile();
const { shouldDefer, effectiveType } = useNetworkAware();import { tierStore, deviceProfileStore, networkAwareStore } from '@adaptive-bundle/svelte';
$tierStore; // 'high' | 'low' | 'medium'
$deviceProfileStore; // full DeviceProfile
$networkAwareStore; // { shouldDefer, effectiveType }The detection engine scores device capability using 5 hardware probes:
| Probe | Weight | Source |
|---|---|---|
| CPU cores | 0.35 | navigator.hardwareConcurrency |
| Device memory | 0.35 | navigator.deviceMemory |
| GPU tier | 0.15 | WebGL renderer string heuristic |
| Screen | 0.10 | Resolution x device pixel ratio |
| Touch points | 0.05 | navigator.maxTouchPoints |
The composite score (0-1) determines the tier: >= 0.5 is high, < 0.5 is low. When probes are unavailable (e.g., deviceMemory on Firefox), weights redistribute automatically.
A fast-path covers ~70% of devices without expensive probing: Data Saver on forces low; cached tier reuses prior result; deviceMemory <= 2GB goes low immediately; >= 8GB with 8+ cores goes high immediately. Full scoring runs only for ambiguous devices.
Asymmetric hysteresis prevents tier flipping near threshold boundaries: low-to-high requires the score to exceed threshold by 0.12; high-to-low requires falling below by 0.08.
Detection completes in < 50ms on any device. Results are cached in localStorage with a 7-day TTL and auto-invalidate when configuration changes.
Network conditions are tracked separately from hardware tier. A high-end phone on 2G should defer heavy loading:
const { shouldDefer, effectiveType } = useNetworkAware();
// shouldDefer: true on 2g/slow-2g
// effectiveType: '4g' | '3g' | '2g' | 'slow-2g'When Data Saver is active, the tier is forced to low regardless of hardware capability.
Adaptive is the only adaptive loading tool with first-class STB/CTV support. Three strategies available depending on your build pipeline:
targetTier— Build per platform, tree-shake unused variant at compile time. Zero runtime cost. Recommended for per-platform builds.deviceMap— Single build, multiple platforms. Static lookup table bypasses scoring.- Custom probe providers — Feed native platform APIs (Tizen, webOS, Sky) into the scoring engine.
See the full STB/CTV Platform Guide.
Quick example for per-platform builds:
adaptive({
targetTier: process.env.PLATFORM === 'foxtel' ? 'low' : 'high',
});PLATFORM=foxtel pnpm build # low-tier-only bundle, no @adaptive-bundle/coreFor devices within the same tier that support different features, platformTierMap adds capability-based build-time pruning:
adaptive({
platformTierMap: {
'foxtel-iq4': { tier: 'low', capabilities: ['drm', 'hdr10'] },
'sky-q': { tier: 'low', capabilities: ['drm', 'dolby-vision'] },
ios: { tier: 'high', capabilities: ['haptics', 'webgl2'] },
android: { tier: 'high', capabilities: ['webgl2', 'nfc'] },
},
});Components declare required capabilities — the plugin prunes chunks at build time when no device in a tier supports them:
const DolbyPlayer = adaptive({
high: () => import('./DolbyPlayer'),
low: () => import('./DolbyPlayer'),
requires: ['dolby-vision'],
capabilityFallback: () => import('./StandardPlayer'),
});At runtime, query the resolved capabilities:
import { getCapabilities } from '@adaptive-bundle/core';
const caps = getCapabilities(); // ['drm', 'dolby-vision'] or []The plugin ships a CLI for standalone analysis, scaffolding, and validation:
npx adaptive analyze # scan source for boundaries
npx adaptive init src/Heavy.tsx # scaffold adaptive boundary
npx adaptive simulate src/X.tsx # what-if analysis
npx adaptive report # regenerate from cached data
npx adaptive validate # check boundary correctness (CI-friendly)See the full CLI Reference.
import('@adaptive-bundle/devtools').then((d) => d.init());Shows current tier, score, confidence, all probe values, reasoning chain, per-boundary decisions, and a tier simulator dropdown. Automatically stripped from production builds.
Visit http://localhost:5173/__adaptive during development. Shows boundary table with sizes, dependency trees, and a tier simulator with HMR-based live reload.
The plugin generates build reports in three formats:
adaptive({
report: true,
reportFormat: 'console', // default — also 'html' or 'json'
reportDir: './adaptive-reports',
});- Console — boundary summary, sizes, savings, opportunities ranked by impact
- HTML — interactive dashboard for stakeholders with trend charts via
history.json - JSON — structured data for CI pipelines and custom tooling
Fail or warn the build when bundles exceed size targets:
adaptive({
budget: {
maxLowTierBundle: 150_000, // max bytes for low-tier total
maxHighVariant: 80_000, // max bytes per high variant
minSavingsPercent: 10, // minimum savings to justify a boundary
enforce: 'error', // 'error' fails build, 'warn' logs only
},
});Boundaries automatically retry failed imports after 1 second. If a high-tier variant fails to load, the low variant is loaded as fallback. The onError callback lets you hook into error tracking:
adaptive({
high: () => import('./RichEditor'),
low: () => import('./BasicEditor'),
onError: (error, boundaryName) => analytics.track('adaptive_error', { error, boundaryName }),
});Basic setup works out of the box. For full options — scoring weights, thresholds, hysteresis, caching, network behavior, SSR defaults, and more — see the Configuration Reference.
Resolve tier from Client Hints headers to avoid the 50ms client-side detection cost entirely:
import { resolveTierFromHeaders } from '@adaptive-bundle/core/server';
// Express / any Node.js server
app.use((req, res, next) => {
const tier = resolveTierFromHeaders(req.headers);
// Uses Sec-CH-Device-Memory and Sec-CH-UA-Mobile headers
});Works in any JS server environment including edge runtimes (Cloudflare Workers, Vercel Edge Functions, Deno Deploy). Zero DOM dependencies.
import { setForcedTier, clearForcedTier } from '@adaptive-bundle/core/testing';
beforeEach(() => setForcedTier('low'));
afterEach(() => clearForcedTier());Or via URL parameter: ?adaptive_tier=low
Use npx adaptive validate in CI to check boundary correctness.
// next.config.js
const { withAdaptive } = require('@adaptive-bundle/next');
module.exports = withAdaptive({
adaptive: {
report: true,
reportFormat: 'console',
},
});The Webpack plugin runs analysis at build time (production only, client-side) using the same analysis engine as the Vite plugin. It creates splitChunks.cacheGroups to isolate tier-specific code.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@adaptive-bundle/nuxt'],
adaptive: {
report: true,
clientHints: true, // auto-injects Nitro middleware for Client Hints
},
});The Nuxt module auto-injects the Vite plugin and registers Nitro server middleware that resolves device tier from Client Hints headers and sets an adaptive_tier_hint cookie.
Adaptive is 100% local, zero-telemetry, zero-network. No data ever leaves the user's device or build environment. This is a hard architectural constraint, not a configurable option. Detection uses only local browser APIs (navigator.hardwareConcurrency, navigator.deviceMemory, WebGL). Cached data lives in localStorage (or memory-only for strict consent policies via cacheStorage: 'memory'). Compatible with GDPR, ePrivacy, and operator-specific STB/CTV privacy requirements.
packages/
core/ @adaptive-bundle/core Detection + scoring (~3KB gzipped, zero deps)
vite-plugin/ @adaptive-bundle/vite-plugin Build analysis, chunk splitting, CLI, reports
react/ @adaptive-bundle/react adaptive() + hooks + Adaptive.High/Low
vue/ @adaptive-bundle/vue adaptive() + composables + AdaptiveHigh/Low
svelte/ @adaptive-bundle/svelte adaptive() + stores + context
next/ @adaptive-bundle/next Next.js Webpack plugin (reuses vite-plugin analysis)
nuxt/ @adaptive-bundle/nuxt Nuxt module + Nitro middleware
devtools/ @adaptive-bundle/devtools Browser overlay + dev dashboard
Chunk isolation guarantee: no code from the high variant leaks into the low-tier bundle. Exclusive dependencies are isolated into separate chunks. Shared dependencies stay in common chunks without duplication.
pnpm install
pnpm build
pnpm test
pnpm typecheck
pnpm lintThe adaptive loading pattern was validated by Google Chrome Labs in 2019 but never shipped as production tooling. Existing solutions provide pieces of the puzzle but not a complete system:
| Adaptive | react-adaptive-hooks | UA parsing libs | Manual if(isMobile) |
|
|---|---|---|---|---|
| Build-time analysis | Yes — automatic | No | No | No |
| Chunk isolation guarantee | Yes — per tier | No | No | No |
| Device hardware scoring | Yes — 5 probes | Partial — raw hooks | UA string only | Manual |
| Framework adapters | React, Vue, Svelte | React only | None | Manual |
| Meta-framework support | Next.js, Nuxt | No | No | No |
| STB/CTV support | First-class | No | Partial | Manual |
| CI budget enforcement | Yes | No | No | No |
| Runtime size | ~3KB gzipped | ~8KB | Varies | 0 |
| Maintained (2026) | Yes | Abandoned (2020) | Varies | N/A |
Key difference: Adaptive is a build intelligence tool that happens to include a runtime. Others are runtime-only libraries that require manual build configuration. Adaptive analyzes your dependency graph, isolates tier-specific code into separate chunks, and guarantees no high-tier code leaks into low-tier bundles — all automatically.
| Package | Budget | Enforced |
|---|---|---|
@adaptive-bundle/core |
3KB gzipped | CI blocks PR |
@adaptive-bundle/react |
2KB gzipped | CI blocks PR |
@adaptive-bundle/vue |
2KB gzipped | CI blocks PR |
@adaptive-bundle/svelte |
1.5KB gzipped | CI blocks PR |
MIT