> ## Documentation Index
> Fetch the complete documentation index at: https://wemstudios.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Blaze Router Benchmarks, Bundle Size, and Cold Start Tips

> Blaze's TrieRouter matches routes in O(log n) time with a ~11 KB gzipped bundle. Learn how to minimize cold starts on Cloudflare's v8 isolate model.

Blaze is optimized for Cloudflare's v8 isolate model. Route registration is synchronous at module startup — the trie is built once when your Worker's isolate initializes, then amortized over every subsequent request served by that isolate. You pay the registration cost exactly once per datacenter warm-up, and matching is O(log n) for the lifetime of the isolate.

## Router benchmarks

The table below compares route-matching throughput at 50 registered routes, measured in microseconds per iteration (lower is better). Blaze's TrieRouter sits between the two Hono routers in absolute speed while delivering better worst-case scaling than itty-router or Express.

| Router                   | µs / iter   | Notes                                                   |
| ------------------------ | ----------- | ------------------------------------------------------- |
| **Blaze TrieRouter**     | **0.41 µs** | O(log n) trie — scales cleanly with route count         |
| Hono RegExpRouter        | 0.38 µs     | Fastest single measurement, but limited pattern support |
| Hono SmartRouter         | 0.44 µs     | Adaptive selection, heavier startup cost                |
| itty-router              | 1.90 µs     | Array scan with regex — simple but degrades linearly    |
| Express (path-to-regexp) | 3.20 µs     | Linear scan — performance cliff grows with route count  |

The key insight is **scaling behaviour**, not just the 50-route snapshot. Express and itty-router use linear scans — each additional route adds cost to every request. Blaze's trie depth grows logarithmically, so adding your 200th route barely moves the needle.

<Note>
  The v8 JIT compiles the trie on first warm-up. After a handful of requests in the same isolate, the hot path is fully compiled — subsequent requests pay zero route registration cost and benefit from optimised native code. The µs/iter figures above represent post-JIT steady-state performance.
</Note>

## Bundle size

A smaller bundle means a faster cold start. Blaze ships no npm dependencies — the trie router, middleware compose, and response helpers are all hand-rolled.

| Framework        | Bundle size                                         |
| ---------------- | --------------------------------------------------- |
| **Blaze core**   | **\~11 KB** minified + gzip                         |
| Hono tiny preset | \~14 KB minified + gzip                             |
| Hono default     | \~18 KB minified + gzip                             |
| Express          | \~572 KB (Node.js only — incompatible with Workers) |

Blaze is \~21% smaller than Hono's tiny preset and \~39% smaller than Hono's default build at equivalent feature coverage.

## Cold start tips

Cold starts happen when Cloudflare spins up a fresh isolate for your Worker. The v8 isolate has to parse and evaluate your module before it can handle the first request. Keep these rules in mind to stay fast:

<Steps>
  ### Use Module Worker syntax

  Export `{ fetch }` directly — do **not** use `addEventListener('fetch', ...)`. Module Workers have lower cold-start latency and give you native access to `env` bindings without a closure.

  ```ts theme={null}
  // ✅ Module Worker — lower cold start, native env access
  export default { fetch: app.fetch };

  // ❌ Service Worker syntax — deprecated, higher latency
  addEventListener('fetch', (event) => {
    event.respondWith(app.fetch(event.request));
  });
  ```

  ### Tree-shake middleware via subpath imports

  Import only the middleware you use. Each middleware lives at its own subpath export so bundlers can eliminate the rest.

  ```ts theme={null}
  // ✅ Only cors.js is bundled — other middleware are tree-shaken
  import { cors }   from 'blaze/middleware/cors';
  import { logger } from 'blaze/middleware/logger';

  // ❌ Imports everything from the barrel — no tree-shaking possible
  import * as blaze from 'blaze';
  ```

  ### Keep module-level code synchronous

  Any `await` at the top level of your module delays the first request. Move async initialization inside route handlers and cache results in module-level variables.

  ```ts theme={null}
  // ✅ Module-level variable — set synchronously at init time
  const app = createApp<Env>();

  // ✅ Lazy cache pattern — async work happens on first request, then cached
  let schemaVersion: string | null = null;

  app.get('/version', async (req, res) => {
    schemaVersion ??= await req.env.DB
      .prepare('SELECT version FROM schema_info')
      .first<string>();
    res.json({ version: schemaVersion });
  });
  ```
</Steps>

## Background tasks with waitUntil

Use `req.ctx.waitUntil()` for fire-and-forget work that should not delay your response — analytics writes, cache warming, logging to an external pipeline. The Worker runtime keeps the isolate alive until all `waitUntil` promises settle, but the `Response` is sent to the client immediately.

```ts theme={null}
app.get('/products/:id', async (req, res) => {
  const product = await req.env.DB
    .prepare('SELECT * FROM products WHERE id = ?')
    .bind(req.params.id)
    .first();

  if (!product) return res.status(404).json({ error: 'Not found' });

  // ① Send the response now — client receives it immediately
  res.json(product);

  // ② Schedule analytics write — runs after response is sent
  req.ctx.waitUntil(
    req.env.ANALYTICS.writeDataPoint({
      blobs:   [req.params.id, req.cf?.country ?? 'unknown'],
      doubles: [Date.now()],
      indexes: ['product-view'],
    })
  );
});
```

Common `waitUntil` patterns:

| Use case              | What to schedule                                             |
| --------------------- | ------------------------------------------------------------ |
| Analytics / telemetry | Write to Workers Analytics Engine or an external sink        |
| Cache warming         | Populate KV after a cache miss so the next request is fast   |
| Audit logs            | Write to D1 or a log drain without blocking the API response |
| Queue enqueue         | `req.env.JOBS.send(payload)` when the job is informational   |

## KV and D1 optimization tips

<Tabs>
  <Tab title="KV">
    **Isolate-level caching** — KV reads are fast (\~1 ms from Cloudflare's edge), but if you read the same key on every request you can do better. Store hot values in a module-level `Map` and refresh them with `waitUntil` in the background.

    ```ts theme={null}
    // Module-level isolate cache — lives for the lifetime of the isolate
    const localCache = new Map<string, { value: string; exp: number }>();

    async function getWithCache(kv: KVNamespace, key: string): Promise<string | null> {
      const cached = localCache.get(key);
      if (cached && cached.exp > Date.now()) return cached.value;

      const value = await kv.get(key);
      if (value) {
        localCache.set(key, { value, exp: Date.now() + 60_000 }); // 60 s TTL
      }
      return value;
    }

    app.get('/config', async (req, res) => {
      const value = await getWithCache(req.env.SESSION_KV, 'global-config');
      res.json({ value });
    });
    ```

    **Use typed `get` calls** — pass `{ type: 'json' }` instead of `JSON.parse(await kv.get(...))` to let the KV client handle deserialization:

    ```ts theme={null}
    const config = await req.env.SESSION_KV.get<Config>('config', { type: 'json' });
    ```
  </Tab>

  <Tab title="D1">
    **Batch multi-statement operations** — use `D1Database.batch()` for multiple mutations that should be atomic. A batch is a single round-trip to D1 instead of one per statement.

    ```ts theme={null}
    app.post('/users/:id/setup', async (req, res) => {
      const { name, email } = await req.json<{ name: string; email: string }>();

      // Single round-trip — all three statements are atomic
      const [user, , profile] = await req.env.DB.batch([
        req.env.DB.prepare('UPDATE users SET name = ? WHERE id = ?')
                  .bind(name, req.params.id),

        req.env.DB.prepare('INSERT INTO audit_log (user_id, action) VALUES (?, ?)')
                  .bind(req.params.id, 'setup'),

        req.env.DB.prepare('SELECT * FROM users WHERE id = ?')
                  .bind(req.params.id),
      ]);

      res.json({ user: profile.results[0] });
    });
    ```

    **Avoid N+1 queries** — fetch related rows in one query with a JOIN rather than looping and querying inside a handler:

    ```ts theme={null}
    // ✅ One query — JOIN fetches posts + author in one round-trip
    const { results } = await req.env.DB
      .prepare(`
        SELECT p.id, p.title, u.name AS author
        FROM   posts p
        JOIN   users u ON u.id = p.author_id
        LIMIT  ?
      `)
      .bind(20)
      .all();
    ```
  </Tab>
</Tabs>

<Tip>
  Apply the `compress()` middleware only to routes that return text responses larger than approximately 1 KB. Compressing small JSON payloads (a few hundred bytes) costs more CPU than it saves in bytes — the overhead of Brotli/gzip encoding exceeds the transfer benefit. Scope it to specific routes or check `Content-Length` before compressing:

  ```ts theme={null}
  import { compress } from 'blaze/middleware/compress';

  // ✅ Scoped to large-response routes only
  app.get('/reports/:id', compress(), generateReport);
  app.get('/export/csv',  compress(), exportCsv);

  // Not applied globally — small API responses stay uncompressed
  app.get('/users/:id', getUser);
  ```
</Tip>
