Skip to content

Performance

Blaze-NG is designed for speed. Here's how it achieves fast rendering, the real benchmark numbers, and how to get the most out of it.

Architecture for Performance

Minimal DOM Operations

Blaze uses a fine-grained reactive system that tracks exactly which DOM nodes depend on which data. When data changes, only the affected nodes update:

Data Change → Reactive Dependency → Specific DOM Node → Targeted Update

No virtual DOM diffing. No component tree reconciliation. Just direct, surgical DOM updates.

Efficient List Rendering

The observe-sequence package implements an O(n+m) diffing algorithm that detects:

  • Insertions — adds only the new elements
  • Removals — removes only the deleted elements
  • Moves — repositions without re-rendering
  • Changes — updates only the modified elements
ts
// Each item has an _id — Blaze tracks by identity
Template.list.helpers({
  items() {
    return Items.find({}, { sort: { position: 1 } });
  },
});

When you add an item to a list of 1000, only 1 DOM node is created. Not 1000.

Benchmark Results

All benchmarks run with Vitest bench (tinybench) in a JSDOM environment. Run them yourself with:

bash
pnpm bench:run

First Render

How fast templates go from source → DOM:

Operationops/secMean
Static div12,0370.08 ms
Div with interpolation14,1110.07 ms
Div with multiple attrs10,0330.10 ms
Card component (10 elements)3,4460.29 ms
10-item list3,3740.30 ms
100-item list6281.59 ms
1,000-item list6116.3 ms
1,000 items × 3 columns2342.6 ms
3-level template nesting4,2660.23 ms

Reactive Updates

How fast data changes propagate to the DOM:

Operationops/secMean
Single text update9,3930.11 ms
3 values + flush8,6950.12 ms
#if toggle2,8320.35 ms
Nested dep (outer)6,9630.14 ms
Nested dep (inner)7,6700.13 ms
Nested dep (both)7,2080.14 ms
10 updates, single flush12,1310.08 ms
100 updates, single flush12,1510.08 ms

Batched flushes are free

Note that 100 sequential ReactiveVar.set() calls followed by a single flush() is just as fast as 10 — the reactive system coalesces updates automatically.

Attribute Updates

Operationops/secMean
Class toggle7,1970.14 ms
Style color update11,6010.09 ms
Data attribute update11,2670.09 ms
3 attributes + flush9,9300.10 ms
Toggle class on 100 elements1995.03 ms

List Operations (Each)

Operationops/secMean
Create 50 rows1,2870.78 ms
Create 100 rows7801.28 ms
Append 1 row to 1005031.99 ms
Append 10 rows to 1005371.86 ms
Prepend 1 row to 1006201.61 ms
Remove 1 from 1001895.28 ms
Remove all 100 rows3682.72 ms
Swap 2 rows in 1004323.4 ms
Update every 10th row4592.18 ms
Reverse 100 rows1565.2 ms

Template Lifecycle

Operationops/secMean
Simple create/destroy10,3130.10 ms
With helpers10,7170.09 ms
2-level nesting6,4970.15 ms
3-level nesting4,5560.22 ms
0 callbacks11,8010.08 ms
3 callbacks (created+rendered+destroyed)12,2680.08 ms
5 onCreated callbacks11,9710.08 ms
10 create/destroy cycles5,0320.20 ms
50 create/destroy cycles1,1770.85 ms

Lifecycle callbacks are essentially free

Adding onCreated, onRendered, and onDestroyed callbacks adds no measurable overhead to the create/destroy cycle.

Compilation Speed

Operationops/secMean
Simple template89,2620.011 ms
Medium (if/each/helpers)5,8640.17 ms
Complex (nested blocks)1,8890.53 ms
50 static rows9101.10 ms
200 static rows2254.44 ms

Sequence Diffing

The observe-sequence diff algorithm:

Operationops/secMean
100 identical items (no-op)147,9770.007 ms
1,000 identical items (no-op)16,0540.06 ms
5,000 identical items (no-op)2,4770.40 ms
Append 10 → 100135,9440.007 ms
Append 100 → 1,00013,5060.07 ms
Shuffle 100 items140,9170.007 ms
Shuffle 1,000 items15,0110.07 ms
Full replace 100121,2360.008 ms
Full replace 1,0008,2670.12 ms
Mixed ops on 1,00014,1290.07 ms

Why Blaze-NG Excels at Updates

React/Vue:   Change data → diff virtual DOM tree → patch real DOM
Blaze-NG:    Change data → reactive system notifies → update specific DOM node

For full page renders, all frameworks perform similarly. Blaze-NG excels at partial updates since there's no diffing overhead — changes go straight to the affected DOM nodes.

Bundle Size

Actual measured sizes (ESM, gzip level 9):

PackageRawGzip
@blaze-ng/core34.7 KB11.6 KB
@blaze-ng/htmljs9.2 KB3.6 KB
@blaze-ng/spacebars2.6 KB1.2 KB
@blaze-ng/observe-sequence4.7 KB2.1 KB
@blaze-ng/spacebars-compiler17.6 KB6.4 KB
@blaze-ng/templating-runtime2.9 KB1.4 KB
@blaze-ng/templating-compiler0.2 KB0.2 KB
Core runtime total51.3 KB18.5 KB
All packages total223.1 KB63.6 KB

Run pnpm bundle-size to measure yourself.

Zero runtime dependencies

Blaze-NG has zero runtime dependencies. No jQuery, no lodash, no uglify-js. Every byte in the bundle is Blaze-NG code.

Old vs New: Head-to-Head Comparison

These benchmarks run identical operations through both the original Meteor Blaze engine and Blaze-NG, side by side in the same Vitest bench suite. Run them yourself:

bash
pnpm bench:compare

Compilation Speed (Template → JS Code)

TemplateOriginal BlazeBlaze-NGRatio
Simple (<div>{{name}}</div>)280K ops/sec133K ops/secOriginal 2.1×
Medium (if/each/helpers)11.9K ops/sec5.7K ops/secOriginal 2.1×
Complex (nested blocks, unless, with)2.7K ops/sec1.5K ops/secOriginal 1.8×
Large (50-row table)857 ops/sec835 ops/sec~Tied

Compilation happens once

Template compilation typically runs at build time or once at startup. Even the "slower" Blaze-NG compiler compiles a complex template in 0.53 ms — imperceptible to users. Runtime performance matters far more.

Parsing Speed (Template → AST)

TemplateOriginal BlazeBlaze-NGRatio
Simple636K ops/sec369K ops/secOriginal 1.7×
Medium21.3K ops/sec14.5K ops/secOriginal 1.5×
Complex8.2K ops/sec6.1K ops/secOriginal 1.4×
Pure HTML (no template tags)31.0K ops/sec33.6K ops/secNG 1.08×

Blaze-NG is faster on pure HTML

When there are no Spacebars template tags, Blaze-NG's TypeScript parser is 8% faster than the original — thanks to sticky regex matching and direct input access that eliminate substring allocations.

HTML Rendering (HTMLjs Tree → HTML String)

OperationOriginal BlazeBlaze-NGRatio
Simple element2.52M ops/sec2.60M ops/secNG 1.03×
Medium tree (nested)382K ops/sec440K ops/secNG 1.15×
Large table (100 rows)6.5K ops/sec6.8K ops/secNG 1.06×
Deeply nested (50 levels)61.0K ops/sec63.1K ops/secNG 1.03×
Build + render medium301K ops/sec332K ops/secNG 1.10×
Build + render large (100 rows)4.5K ops/sec5.0K ops/secNG 1.12×

Blaze-NG wins where it matters

For the end-to-end path users actually experience — constructing an HTML tree and rendering it — Blaze-NG is 3–15% faster than the original across the board. And this is before Blaze-NG's advantage in reactive updates, which skip DOM diffing entirely.

Summary

LayerOverall Result
Compilation (build-time)Original ~2× faster on small/medium; ~tied on large templates
Parsing (build-time)Original 1.4–1.7× faster; NG 8% faster on pure HTML
HTML Rendering (runtime)Blaze-NG beats Original by 3–15% across the board
Reactive Updates (runtime)Blaze-NG — zero-dependency reactive system with no jQuery overhead
Bundle SizeBlaze-NG 29 KB gzip vs Original's jQuery + Tracker + lodash deps

Internal Engine Optimizations

Under the hood, Blaze-NG's parser, compiler, and rendering pipeline apply several low-level optimizations that reduce memory allocations and CPU overhead on every operation.

Zero-Allocation Regex Matching

The HTML scanner's makeRegexMatcher converts ^-anchored regexes to the sticky (y) flag, matching directly against the input string at the current position. This eliminates a rest() substring allocation on every token match — a significant win since the parser calls regex matchers thousands of times per template.

Direct Input Access

Hot-path functions in the tokenizer (getComment, getDoctype, getTagToken, isLookingAtEndTag) and character reference resolver access scanner.input with charAt/startsWith/indexOf at scanner.pos instead of calling scanner.rest(). This avoids creating intermediate substrings that would immediately become garbage.

Inline Character Code Checks

HTML whitespace detection uses an inline charCodeAt comparison instead of a regex test:

ts
const isHTMLSpace = (ch: string): boolean => {
  if (!ch) return false;
  const c = ch.charCodeAt(0);
  return c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20;
};

This is called in tight loops (attribute parsing, tag scanning, doctype parsing) where regex overhead adds up.

Singleton Visitors

The toHTML(), toJS(), and common toText() functions reuse pre-built singleton visitor instances instead of allocating a new one per call. Since ToHTMLVisitor, ToJSVisitor, and ToTextVisitor are stateless, a single instance is safe to share across all invocations.

Array-Based String Building

The _beautify code formatter builds its output using an array of string parts joined at the end, avoiding O(n²) string concatenation. It tracks lastChar as a scalar rather than indexing into a growing string.

Cached Indentation Strings

The beautifier pre-computes indentation strings (' '.repeat(level)) for nesting levels 0–15, covering virtually all real-world templates without any runtime repeat() calls.

Module-Level Regex Constants

Frequently used regex patterns (identifier validation in tojs, end-tag detection in tokenize) are compiled once at module load and reused across calls, avoiding repeated regex construction inside hot functions.

Substring Slicing in Tokenizer

The beautifier's tokenizeForBeautify uses index tracking (bufStart) and substring slicing to extract text chunks, replacing character-by-character string concatenation with a single allocation per token.

Optimization Tips

1. Use Fine-Grained Templates

Split large templates into smaller ones. Each template creates its own reactive scope:

handlebars
<!-- Bad: entire template re-renders when anything changes -->
<template name="userDashboard">
  <h1>{{user.name}}</h1>
  <p>Messages: {{messageCount}}</p>
  <ul>
    {{#each task in tasks}}
      <li>{{task.text}}{{task.status}}</li>
    {{/each}}
  </ul>
</template>

<!-- Good: each section updates independently -->
<template name="userDashboard">
  {{> userHeader user=user}}
  {{> messageCounter}}
  {{> taskList tasks=tasks}}
</template>

2. Isolate Reactive Helpers

Helpers that return reactive data cause their containing element to re-render:

ts
// Bad: returns a new object every time — triggers unnecessary re-renders
Template.myComponent.helpers({
  style() {
    return {
      color: Session.get('color'),
      fontSize: Session.get('fontSize'),
    };
  },
});

// Good: return primitive values from separate helpers
Template.myComponent.helpers({
  textColor() {
    return Session.get('color');
  },
  fontSize() {
    return Session.get('fontSize');
  },
});

3. Limit Cursor Fields

Only subscribe to and fetch the fields you need:

ts
Template.userList.helpers({
  users() {
    // Bad: fetches all fields
    return Users.find();

    // Good: only fetch displayed fields
    return Users.find(
      {},
      {
        fields: { name: 1, avatar: 1, status: 1 },
        sort: { name: 1 },
        limit: 50,
      },
    );
  },
});

4. Debounce Frequent Updates

For rapidly changing data (e.g., window resize, scroll position):

ts
Template.scrollTracker.onCreated(function () {
  this.scrollY = new ReactiveVar(0);

  let rafId;
  this._onScroll = () => {
    cancelAnimationFrame(rafId);
    rafId = requestAnimationFrame(() => {
      this.scrollY.set(window.scrollY);
    });
  };
  window.addEventListener('scroll', this._onScroll, { passive: true });
});

5. Avoid Unnecessary Reactivity

Use nonReactive when you don't need reactive updates:

ts
Template.report.helpers({
  generatedAt() {
    // Don't re-render when this changes
    return Blaze._nonReactive(() => {
      return new Date().toISOString();
    });
  },
});

6. Use batch for Multiple Updates

When updating multiple reactive variables at once:

ts
Template.form.events({
  'click .reset'(event, instance) {
    // Bad: causes 3 separate re-renders
    instance.name.set('');
    instance.email.set('');
    instance.age.set(0);

    // Good: single re-render
    Blaze.batch(() => {
      instance.name.set('');
      instance.email.set('');
      instance.age.set(0);
    });
  },
});

7. Pre-compile Templates

Compile templates at build time instead of runtime:

ts
// Runtime compilation (slower startup)
const renderFn = SpacebarsCompiler.compile(templateString);

// Build-time compilation (faster startup)
// Use @blaze-ng/templating-tools in your build pipeline
import { TemplatingTools } from '@blaze-ng/templating-tools';

const compiled = TemplatingTools.compileTagsWithSpacebars([
  { tagName: 'template', attribs: { name: 'myTemplate' }, contents: html },
]);

8. Lazy Load Templates

Load templates on demand for large apps:

ts
// Define a lazy template that loads on first use
const lazyTemplates = {};

async function loadTemplate(name) {
  if (!lazyTemplates[name]) {
    const module = await import(`./templates/${name}.js`);
    lazyTemplates[name] = module.default;
  }
  return lazyTemplates[name];
}

Template.app.helpers({
  async currentPage() {
    const route = Router.current();
    return await loadTemplate(route.template);
  },
});

Profiling

Browser DevTools

  1. Open Chrome DevTools → Performance tab
  2. Record while interacting with your app
  3. Look for:
    • Long "Recalculate Style" events → too many DOM changes
    • Frequent "Layout" events → layout thrashing
    • JavaScript taking >16ms → blocking the main thread

Reactive System Debugging

ts
// Count how many autoruns are active
let activeComputations = 0;

const originalAutorun = reactiveSystem.autorun;
reactiveSystem.autorun = function (fn) {
  activeComputations++;
  const handle = originalAutorun.call(this, fn);
  return {
    stop() {
      activeComputations--;
      handle.stop();
    },
  };
};

// Log periodically
setInterval(() => {
  console.log(`Active computations: ${activeComputations}`);
}, 5000);

Memory Management

Template Cleanup

Templates automatically clean up when destroyed, but watch for:

ts
// Potential leak: external reference keeps computation alive
const liveData = [];

Template.item.onCreated(function () {
  liveData.push(this); // ❌ Reference survives template destruction
});

// Fixed: clean up external references
Template.item.onDestroyed(function () {
  const idx = liveData.indexOf(this);
  if (idx >= 0) liveData.splice(idx, 1);
});

Subscription Management

ts
// Good: subscription auto-stops when template is destroyed
Template.myComponent.onCreated(function () {
  this.subscribe('data'); // Managed by template lifecycle
});

// Bad: manual subscription never stops
Template.myComponent.onCreated(function () {
  Meteor.subscribe('data'); // ❌ Not tied to template lifecycle
});

Released under the MIT License.