Ship the right experience to every device. Automatically.
Stop guessing what your users' devices can handle. DeviceRouter detects real device capabilities — CPU cores, memory, network speed, and more — and gives your server the intelligence to adapt responses instantly.
A ~1 KB client probe. One middleware call. Full device awareness on every request.
Responsive design adapts layout. DeviceRouter adapts what you serve.
- A budget phone on 2G? Skip the heavy animations, defer non-critical components, prefer server-side rendering.
- A flagship on fiber? Go all out — full interactivity, rich visuals.
No user-agent sniffing. No guesswork. Real signals from real devices, classified into actionable tiers and rendering hints your server can act on immediately.
┌──────────┐ ┌────────────┐ ┌─────────┐
│ Browser │ POST /probe │ Express │ │ Storage │
│ (~1 KB) │ ──────────────> │ Middleware │ ──> │ │
│ │ device signals │ │ │ │
└──────────┘ └────────────┘ └─────────┘
│
▼
┌───────────────────┐
│ req.deviceProfile │
│ tiers + hints │
└───────────────────┘
- Probe — A tiny script runs once per session, collecting device signals via browser APIs
- Classify — The middleware classifies the device into CPU, memory, connection, and GPU tiers
- Hint — Rendering hints like
deferHeavyComponentsandreduceAnimationsare derived automatically - Serve — Your route handlers read
req.deviceProfileand respond accordingly
pnpm add @device-router/middleware-express @device-router/storageimport express from 'express';
import { createDeviceRouter } from '@device-router/middleware-express';
import { MemoryStorageAdapter } from '@device-router/storage';
const app = express();
const { middleware, probeEndpoint } = createDeviceRouter({
storage: new MemoryStorageAdapter(),
});
app.use(express.json());
app.post('/device-router/probe', probeEndpoint);
app.use(middleware);
app.get('/', (req, res) => {
const profile = req.deviceProfile;
if (profile?.hints.preferServerRendering) {
return res.send(renderSSR()); // Full server-rendered page
}
if (profile?.hints.deferHeavyComponents) {
return res.send(renderLite()); // Lightweight shell + lazy loading
}
res.send(renderFull()); // Rich interactive experience
});| Signal | Source | Browser Support |
|---|---|---|
| CPU cores | hardwareConcurrency |
All modern browsers |
| Device memory | deviceMemory |
Chrome, Edge |
| Connection type | navigator.connection |
Chrome, Edge |
| Downlink speed | navigator.connection |
Chrome, Edge |
| Round-trip time | navigator.connection |
Chrome, Edge |
| Data saver mode | navigator.connection |
Chrome, Edge |
| Viewport size | window.innerWidth/Height |
All browsers |
| Pixel ratio | devicePixelRatio |
All browsers |
| Prefers reduced motion | matchMedia |
All modern browsers |
| Prefers color scheme | matchMedia |
All modern browsers |
| GPU renderer | WebGL debug info | Chrome, Firefox, Edge |
| Battery status | navigator.getBattery() |
Chrome, Edge |
All signals are optional — the probe gracefully degrades based on what the browser supports.
Note: The probe also collects
navigator.userAgentand viewport dimensions for bot/crawler filtering. They are used during probe submission and stripped before the profile is stored.
Devices are classified across three dimensions:
| Dimension | None | Low | Mid | High/Fast |
|---|---|---|---|---|
| CPU | — | 1–2 cores | 3–4 cores | 5+ cores |
| Memory | — | ≤2 GB | 2–4 GB | >4 GB |
| Connection | — | 2G | 3G / slow 4G | Fast 4G+ (≥5 Mbps) |
| GPU | No WebGL | Software renderer | Integrated / older discrete | RTX, RX 5000+, Apple M-series |
Based on tiers and user preferences, DeviceRouter derives actionable booleans:
| Hint | When it activates |
|---|---|
deferHeavyComponents |
Low-end device, slow connection, or low battery |
serveMinimalCSS |
Low-end device |
reduceAnimations |
Low-end device, prefers reduced motion, or low battery |
useImagePlaceholders |
Slow connection (2G/3G) |
preferServerRendering |
Low-end device |
disable3dEffects |
No GPU or software renderer |
Override the default tier boundaries to match your application's needs:
const { middleware, probeEndpoint } = createDeviceRouter({
storage,
thresholds: {
cpu: { lowUpperBound: 4, midUpperBound: 8 },
memory: { midUpperBound: 8 },
},
});Thresholds are validated at startup — inverted bounds (e.g. lowUpperBound >= midUpperBound), non-positive values, and non-RegExp GPU patterns throw immediately.
Automatically inject the probe script into HTML responses:
// pnpm add @device-router/probe
const { middleware, probeEndpoint, injectionMiddleware } = createDeviceRouter({
storage,
injectProbe: true,
probeNonce: 'my-csp-nonce', // optional CSP nonce
});
// Register injectionMiddleware before your routesNo need to manually add <script> tags — the probe is injected before </head> in every HTML response.
Don't need the full factory? Use the individual pieces directly:
import {
createMiddleware,
createProbeEndpoint,
createInjectionMiddleware,
loadProbeScript,
} from '@device-router/middleware-express';
const middleware = createMiddleware({ storage, thresholds });
const endpoint = createProbeEndpoint({ storage, ttl: 3600 });
const injection = createInjectionMiddleware({
probeScript: loadProbeScript(),
});Each piece validates its own options at creation time. See the API docs for full option tables.
By default, deviceProfile is null on the first page load before the probe runs. Two opt-in strategies provide immediate classification:
const { middleware, probeEndpoint } = createDeviceRouter({
storage,
// Option 1: Classify from User-Agent + Client Hints headers
classifyFromHeaders: true,
// Option 2: Fall back to preset defaults
fallbackProfile: 'conservative', // or 'optimistic' or custom DeviceTiers
});When classifyFromHeaders is enabled, mobile/tablet/desktop detection happens from the UA string, and Chromium Client Hints (Device-Memory, Save-Data) refine the result. Check profile.source to know the origin: 'probe', 'headers', or 'fallback'.
See Getting Started — First-Request Handling for details.
Plug in logging and metrics with a single callback — no middleware wrapping needed:
const { middleware, probeEndpoint } = createDeviceRouter({
storage,
onEvent: (event) => {
if (event.type === 'profile:classify') {
logger.info('classified', { source: event.source, cpu: event.tiers.cpu });
}
},
});Events: profile:classify, profile:store, bot:reject, error. See the Observability guide.
| Package | Description | Size |
|---|---|---|
@device-router/probe |
Client-side capability probe | ~1 KB gzipped |
@device-router/types |
Type definitions, classification, and hint derivation | — |
@device-router/storage |
Storage adapters (in-memory + Redis) | — |
@device-router/middleware-express |
Express middleware | — |
@device-router/middleware-fastify |
Fastify plugin | — |
@device-router/middleware-hono |
Hono middleware (edge-compatible) | — |
@device-router/middleware-koa |
Koa middleware | — |
Development — In-memory with automatic TTL expiration:
import { MemoryStorageAdapter } from '@device-router/storage';
new MemoryStorageAdapter();Production — Redis for multi-process / multi-server deployments:
import { RedisStorageAdapter } from '@device-router/storage';
new RedisStorageAdapter(redisClient, { prefix: 'dr:profile:' });Each framework has an example app that renders a product landing page adapting in real time to device capabilities:
- Full experience (high-end device) — animated gradient hero, SVG icons, inline charts, pulsing CTA, hover transitions
- Lite experience (low-end device) — flat solid backgrounds, Unicode icons, placeholder boxes, no animations
Run any example to see it in action:
pnpm install && pnpm build
cd examples/express-basic
pnpm devOpen http://localhost:3000 — the probe runs on first load, refresh to see your detected profile. Use ?force=lite or ?force=full to preview each mode without a real device.
| Framework | Guide | Example |
|---|---|---|
| Express | Quick Start | express-basic |
| Fastify | Quick Start | fastify-basic |
| Hono | Quick Start | hono-basic |
| Koa | Quick Start | koa-basic |
| Observability | Observability guide | observability |
- Getting Started
- Privacy Guide — Signal inventory, cookie details, consent integration, GDPR/CCPA/LGPD guidance
- Observability — Logging, metrics, and monitoring hooks
- Deployment Guide — Docker, Cloudflare Workers, serverless
- Meta-Framework Integration — Next.js, Remix, SvelteKit
- Profile Schema Reference
- API Reference: types | probe | storage | express | fastify | hono | koa
git clone <repo-url>
cd DeviceRouter
pnpm install
pnpm build
pnpm testMIT