This guide covers how to use the VDX optimizer for build-time transformations, linting, and production builds.
# Development linting - find all issues
node optimize.js -i ./app -l
# CI linting - fail on unfixable issues only
node optimize.js -i ./app -l --strict
# Auto-fix simple issues
node optimize.js -i ./app --auto-fix
# Build with optimization
node optimize.js -i ./app -o ./dist
# Build with minification + source maps
node optimize.js -i ./app -o ./dist -m -sThe optimizer transforms your code for fine-grained reactivity without requiring runtime eval():
- Wraps reactive expressions - Transforms
${expr}inhtml`` templates to${html.contain(() => expr)}` - Fixes early dereferences - Inlines
const x = this.state.yinto template expressions - Strips eval(opt()) - Removes redundant runtime wrappers
- Optionally minifies - JavaScript minification with source maps
template() {
const { count } = this.state;
return html`<div>${count}</div>`;
}template() {
return html`<div>${html.contain(() => (this.state.count))}</div>`;
}| Option | Short | Description |
|---|---|---|
--input |
-i |
Input directory (required) |
--output |
-o |
Output directory (required for build) |
--minify |
-m |
Minify JavaScript output |
--sourcemap |
-s |
Generate source maps (implies --minify) |
--wrapped-only |
Only optimize eval(opt()) wrapped templates |
|
--lint-only |
-l |
Check for issues without transforming |
--strict |
With --lint-only: only show unfixable issues | |
--auto-fix |
Fix simple patterns in-place | |
--verbose |
-v |
Show detailed processing info |
--dry-run |
Preview without writing files | |
--help |
-h |
Show help message |
Check all files for issues that break reactivity:
node optimize.js -i ./app -lThis shows both:
- Fixable issues - The optimizer will fix these automatically
- Unfixable issues - You must fix these manually
Exit codes:
0- No issues1- Fixable issues found2- Unfixable issues found
For CI pipelines, use --strict to only fail on issues the optimizer can't fix:
node optimize.js -i ./app -l --strictExit codes:
0- No unfixable issues (code is ready for optimization)1- Unfixable issues found (must fix manually)
Automatically fix simple early dereference patterns:
# Preview fixes
node optimize.js -i ./app --auto-fix --dry-run
# Apply fixes
node optimize.js -i ./app --auto-fixAuto-fix handles:
const x = this.state.y→ replaced withthis.state.yconst { x, y } = this.state→ replaced withthis.state.x,this.state.y
Auto-fix cannot handle:
- Computed expressions:
const x = this.state.y + 1 - Logical operations:
const x = this.state.y || default - Function calls:
const x = fn(this.state.y)
Variables extracted from state before the template:
// ⚠️ FIXABLE - optimizer will inline
const { count } = this.state;
return html`<div>${count}</div>`;Variables captured in arrow functions lose reactivity:
// ⚠️ FIXABLE - optimizer will inline
const { count } = this.state;
${when(condition, () => html`<span>${count}</span>`)}Computed values passed to helpers become stale:
// ❌ UNFIXABLE - must refactor manually
const items = data.slice(this.state.start, this.state.end);
${memoEach(items, item => html`<div>${item.name}</div>`)}Fix: Move the computation inside the helper or use a different pattern:
// ✅ CORRECT - computation inside each iteration
${memoEach(
this.state.data,
(item, idx) => {
if (idx < this.state.start || idx >= this.state.end) return null;
return html`<div>${item.name}</div>`;
},
item => item.id
)}Computed expressions can't be automatically inlined:
// ❌ UNFIXABLE - expression is computed
const displayName = this.state.user?.name || 'Guest';
return html`<div>${displayName}</div>`;Fix: Move computation into template:
// ✅ CORRECT - expression in template
return html`<div>${this.state.user?.name || 'Guest'}</div>`;Optimize without minification:
node optimize.js -i ./app -o ./distOptimize with minification and source maps:
node optimize.js -i ./app -o ./dist -m -sOnly optimize templates explicitly wrapped in eval(opt()):
node optimize.js -i ./app -o ./dist --wrapped-onlyUse this when migrating incrementally or when some templates shouldn't be optimized.
For development without a build step:
import { opt } from './lib/opt.js';
template: eval(opt(() => html`<div>${this.state.count}</div>`))Requires: 'unsafe-eval' in Content-Security-Policy
Run the optimizer as a build step:
node optimize.js -i ./src -o ./dist -m -sBenefits:
- No
'unsafe-eval'CSP requirement - Smaller bundle (no runtime optimizer)
- Better error detection at build time
- Write code normally with
this.state.xin templates - Run linting periodically:
node optimize.js -i ./app -l - Fix unfixable issues as they arise
Add to your pre-commit hook:
node optimize.js -i ./app -l --strict || exit 1# In your CI config
- run: node optimize.js -i ./app -l --strict
- run: node optimize.js -i ./app -o ./dist -m -sWhen using --sourcemap:
- Source maps are generated alongside minified files (
.js.map) - Original source is embedded in
sourcesContent - Identifier names are tracked in the
namesarray for debugger support - Debuggers can map minified code back to original source
Validation: Use the included validation tool to check source maps:
node validate-sourcemaps.js # Validate all dist/ files
node validate-sourcemaps.js --verbose file.js # Detailed validationTo use source maps in the browser:
- Chrome/Firefox DevTools automatically load
.mapfiles - Enable "Enable JavaScript source maps" in DevTools settings
When --sourcemap is enabled:
- JavaScript code is fully minified
- Whitespace in `html`` templates is collapsed (multiple spaces/newlines → single space)
- CSS in
styles:blocks remains readable (full CSS minification skipped) - Source maps accurately point to original source positions
Without --sourcemap, all content (JS, CSS, HTML templates) is fully minified for smallest size.
The optimizer can't inline computed expressions. Refactor to keep reactive expressions in templates:
// ❌ Can't fix
const x = this.state.count * 2;
return html`<div>${x}</div>`;
// ✅ Fixed
return html`<div>${this.state.count * 2}</div>`;If using runtime eval(opt()), you need:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval'">To avoid this, use build-time optimization instead.
These notes are for framework development and maintenance.
Source Map Line Counts Must Match Output: Any transformation that changes line counts must happen BEFORE position tracking:
// ✅ Safe - removes trailing whitespace, preserves line count
code = code.replace(/^[ \t]+$/gm, '');
// ❌ Unsafe before source map generation - changes line count
code = code.replace(/\n{3,}/g, '\n\n');Arrow Function End Detection: Arrow functions with implicit returns spanning multiple lines need special handling:
const foo = x =>
x + 1; // Don't stop parsing here!Track sawArrow state and look for proper terminators (;, }), not newlines.
Re-Export Chain Resolution: When file A re-exports from B which re-exports from C, trace the chain to find the actual binding:
// framework.js: export { h } from './preact/index.js'
// preact/index.js: export { createElement as h }
// Actual binding is: createElementImport Alias Generation: When stripping imports during bundling, generate alias declarations:
// Original: import { render as preactRender } from './render.js';
// Bundle needs: const preactRender = render;Regex Literal Detection in Minifiers: Detect regex vs division by checking the previous token:
- After
=,(,[,,,return,{→ likely regex - After identifier,
),],}→ likely division
Custom Elements Cannot Be Static:
Custom elements must NEVER be marked as fully static, even with no slots or events. They use special _vdxChildren property for children handling that requires dynamic rendering.
Elements with Events Cannot Be Static:
Any event binding (including method-based handlers) prevents static marking. Check for eventDef.slot, eventDef.xModel, AND eventDef.method.
Template Literal Array Reference Identity: Each call site of a tagged template literal produces the SAME array reference every call. This enables O(1) cache lookup using the array itself as the WeakMap key.
HTML Entity Decoding Must Precede URL Sanitization:
Malicious URLs can hide schemes with entities: javascript:alert(1). Decode entities during parsing, then sanitize URLs.
Boolean Attributes Have Complex Rules:
undefined/null/false→ Remove attributetrue→ Set totrue- String
"false"→ Coerces totrue(non-empty string!) - Number
0→false
Synchronous Renders in Effects Prevent Infinite Loops:
Async renders (queueMicrotask) from reactive effects can cause infinite loops. Keep renders synchronous within the effect context.
Property Name Collisions: Before adding internal properties, check for existing usage:
_vdxChildrenis used for props.children_vdxDomChildrenis for parent-child component tracking
Parent-Child Component Tracking Pattern:
Track hierarchy in connectedCallback, clean up in disconnectedCallback:
let parent = this.parentElement;
while (parent) {
if (parent._isVdxComponent) {
this._vdxParent = parent;
parent._vdxDomChildren ??= new Set();
parent._vdxDomChildren.add(this);
break;
}
parent = parent.parentElement;
}- docs/reactivity.md - Reactive state patterns
- docs/templates.md - Template syntax and helpers
- docs/bundles.md - Pre-bundled framework files