Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Sunday, 26 July 2026

LightDraw Diagram Engine: JSON In, Animated Visuals Out

Standard

Hi, this is part 2 of my LightDraw series. Last time we covered the big picture (dashboards, automotive, diagrams, UI). Today we go deep on one feature people keep asking about: diagram wire-flow animation, and how to drive every chart from plain JSON you can store, edit, or generate with AI.

By the end of this post you’ll be able to:

  • Describe a flowchart, pipeline, network, org chart, schematic, or UML class diagram as JSON
  • Pass that JSON to JavaScript and render it with LightDraw
  • Turn on path animation (dashes + packets + status tint) and use the built-in play/pause toolbar
  • Copy working examples without fighting a framework stack

Requires: lightdraw@1.2.1+ for the built-in diagram toolbar when flow.enabled is on.

The mental model (keep this)

Everything in this post follows the same three steps:

  1. Write a scene object: { type, props }
  2. Mount it: Diagram.fromJSON(type, props, app)app.add(chart)
  3. Optional polish: fitToBounds, editor, flow controls
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lightdraw@1/dist/lightdraw.min.css">
<div id="app" style="position:relative; width:800px; height:480px;"></div>
<script src="https://cdn.jsdelivr.net/npm/lightdraw@1/dist/lightdraw.min.js"></script>
<script>
const app = LightDraw.createApp('#app', {
  width: 800,
  height: 480,
  renderer: 'canvas',
  background: '#0f172a',
});

function mountDiagram(scene) {
  app.clear();
  const chart = LightDraw.Diagram.fromJSON(scene.type, scene.props, app);
  app.add(chart);
  LightDraw.Diagram.fitToBounds(chart, 800, 480, 24);
  return chart;
}

// later: mountDiagram(yourScene);
</script>

That’s the whole contract. Your diagrams live as JSON. JS only loads and renders them.

1. Flowchart: decisions, paths, and motion

Figure: Flowchart with multi-path wire flow and status tint (idle → active → done).

A flowchart is nodes + edges. Animation is just an ordered list of node ids under flow.paths.

const flowchartScene = {
  type: 'flowchart',
  props: {
    width: 800,
    height: 480,
    data: {
      nodes: [
        { id: 'start', label: 'Start', type: 'start', x: 360, y: 24 },
        { id: 'check', label: 'Valid?', type: 'decision', x: 360, y: 110 },
        { id: 'process', label: 'Process', type: 'process', x: 360, y: 210 },
        { id: 'notify', label: 'Notify', type: 'process', x: 140, y: 210 },
        { id: 'end', label: 'Done', type: 'end', x: 360, y: 320 },
      ],
      edges: [
        { from: 'start', to: 'check' },
        { from: 'check', to: 'process', label: 'Yes' },
        { from: 'check', to: 'notify', label: 'No' },
        { from: 'process', to: 'end' },
        { from: 'notify', to: 'end' },
      ],
    },
    flow: {
      enabled: true,
      mode: 'both',          // 'dash' | 'packet' | 'both'
      playback: 'loop',      // or 'once'
      highlight: 'pulse',
      statusHighlight: true,
      speed: 1.5,
      pathGapMs: 400,
      paths: [
        ['start', 'check', 'process', 'end'],
        ['start', 'check', 'notify', 'end'],
      ],
    },
  },
};

mountDiagram(flowchartScene);

Tip: Status tint needs packet or both. Grey = not yet visited, yellow = current hop, green = done for this path run.

2. Process pipeline: CI / ETL stages

Figure: Process pipeline stages animating in order.

const pipelineScene = {
  type: 'processPipeline',
  props: {
    width: 800,
    height: 280,
    stages: [
      { id: 'ingest', label: 'Ingest', status: 'done', type: 'input' },
      { id: 'validate', label: 'Validate', status: 'done', type: 'test' },
      { id: 'build', label: 'Build', status: 'active', type: 'build' },
      { id: 'test', label: 'Test', status: 'pending', type: 'test' },
      { id: 'deploy', label: 'Deploy', status: 'pending', type: 'deploy' },
    ],
    flow: {
      enabled: true,
      mode: 'both',
      playback: 'loop',
      statusHighlight: true,
      path: ['ingest', 'validate', 'build', 'test', 'deploy'],
    },
  },
};

mountDiagram(pipelineScene);

Use a single path for one run, or paths: [['a','b'], ['a','c']] for alternating runs (same as flowchart).

3. Network topology: traffic between devices

Figure: Network icons with two traffic paths (edge → web, edge → API → DB).

const networkScene = {
  type: 'networkTopology',
  props: {
    width: 800,
    height: 400,
    data: {
      nodes: [
        { id: 'inet', label: 'Internet', type: 'cloud', x: 360, y: 24 },
        { id: 'fw', label: 'NGFW', type: 'ngfw', x: 360, y: 130 },
        { id: 'web', label: 'Web', type: 'server', x: 160, y: 260 },
        { id: 'api', label: 'API', type: 'server', x: 360, y: 260 },
        { id: 'db', label: 'SQL', type: 'sql_database', x: 560, y: 260 },
      ],
      edges: [
        { from: 'inet', to: 'fw' },
        { from: 'fw', to: 'web' },
        { from: 'fw', to: 'api' },
        { from: 'api', to: 'db' },
      ],
    },
    flow: {
      enabled: true,
      mode: 'both',
      playback: 'loop',
      paths: [
        ['inet', 'fw', 'web'],
        ['inet', 'fw', 'api', 'db'],
      ],
    },
  },
};

mountDiagram(networkScene);

Node type values map to a Visio/Cisco-style catalog (server, router, ngfw, sql_database, cloud, …). Perfect for NOC boards and security runbooks.

4. Animation properties & controls (the JSON knobs)

Figure: Built-in toolbar (▶⏸↻ + zoom + Fit) with path status tint.

All of these live under props.flow and round-trip with Diagram.toJSON / fromJSON:

PropertyValuesWhat it does
enabledtrue / falseStart wire-flow animation
modedash · packet · bothMarching dashes, traveling dot, or both
playbackloop · onceRepeat forever, or play then pause
paths / patharray of node idsOrdered hops to animate
speednumber (e.g. 1.5)Playback rate
pathGapMsmsPause between path runs
highlightpulse · breathe · flash · noneMotion chrome on active hops
statusHighlightboolIdle / active / done color tint
statusColorsobjectOverride idle/active/done/error colors
chrometrue (default) / falseShow ▶⏸↻ + zoom overlay

Control from JS (same chart you mounted):

LightDraw.Diagram.applyFlow(app, chart, { /* same options as flow */ });
LightDraw.Diagram.pauseFlow(app, chart);
LightDraw.Diagram.resumeFlow(app, chart);
LightDraw.Diagram.toggleFlowPause(app, chart);
LightDraw.Diagram.replayFlow(app, chart);
LightDraw.Diagram.stopFlow(chart);

// Manual toolbar if you disabled chrome in JSON:
// LightDraw.Diagram.installToolbar(app, chart);
// LightDraw.Diagram.uninstallToolbar(chart);

Give the host #app { position: relative; } so the toolbar overlays correctly.

5. Org chart: hierarchy from a nested tree

Figure: Org chart rendered from a nested root JSON tree.

No edges array here, just a tree. Same mount helper.

const orgScene = {
  type: 'orgChart',
  props: {
    width: 900,
    height: 480,
    root: {
      name: 'Alex Rivera',
      role: 'CEO',
      children: [
        {
          name: 'Sam Chen',
          role: 'CTO',
          department: 'Engineering',
          children: [
            { name: 'Priya N.', role: 'Platform Lead' },
            { name: 'Jordan K.', role: 'Frontend Lead' },
          ],
        },
        {
          name: 'Morgan Lee',
          role: 'CFO',
          department: 'Finance',
          children: [{ name: 'Riley P.', role: 'Controller' }],
        },
        { name: 'Casey Brooks', role: 'COO', department: 'Ops' },
      ],
    },
  },
};

mountDiagram(orgScene);

// Optional: drag / resize / collapse in the editor
LightDraw.Diagram.installEditor(app, chart, {
  mode: 'arrange',
  allowResize: true,
});

6. Schematic diagram: IEC symbols as JSON

Figure: Battery → switch → resistor → LED → ground from a component list.

const schematicScene = {
  type: 'electricalSchematic',
  props: {
    width: 800,
    height: 360,
    components: [
      { id: 'bat', type: 'battery', x: 80, y: 120, label: 'BAT' },
      { id: 'sw', type: 'spst', x: 220, y: 120, label: 'S1' },
      { id: 'r1', type: 'resistor', x: 360, y: 120, label: 'R1' },
      { id: 'led', type: 'led', x: 500, y: 120, label: 'D1' },
      { id: 'gnd', type: 'ground', x: 500, y: 220, label: 'GND' },
    ],
  },
};

mountDiagram(schematicScene);

Symbol kinds come from the IEC catalog (spst, nmos, opAmp, led, …). Discover them with LightDraw.Diagram.listSchematicSymbols().

7. UML class diagram: structure + animated relations

Figure: Class boxes with inheritance paths animated for a walkthrough.

const umlScene = {
  type: 'classDiagram',
  props: {
    width: 800,
    height: 400,
    data: {
      classes: [
        { id: 'drawable', name: 'Drawable', x: 80, y: 40, methods: ['draw()'], stereotype: 'interface' },
        { id: 'shape', name: 'Shape', x: 360, y: 40, attrs: ['id: string'], methods: ['draw()'] },
        { id: 'rect', name: 'Rect', x: 220, y: 240, attrs: ['w', 'h'], methods: ['draw()'] },
        { id: 'circle', name: 'Circle', x: 520, y: 240, attrs: ['r'], methods: ['draw()'] },
      ],
      relations: [
        { from: 'shape', to: 'drawable', type: 'realization' },
        { from: 'rect', to: 'shape', type: 'inheritance' },
        { from: 'circle', to: 'shape', type: 'inheritance' },
      ],
    },
    flow: {
      enabled: true,
      mode: 'both',
      playback: 'loop',
      paths: [
        ['rect', 'shape', 'drawable'],
        ['circle', 'shape', 'drawable'],
      ],
    },
  },
};

mountDiagram(umlScene);

Load JSON from a file or API

Because the scene is just data, you can keep charts next to configs:

// From a static file
const scene = await fetch('/scenes/onboarding-flow.json').then((r) => r.json());
mountDiagram(scene);

// Or via the app helper when the diagram plugin is registered
app.loadJSON(scene);

// Round-trip after the user edits in the canvas
const saved = LightDraw.Diagram.toJSON(chart);
localStorage.setItem('my-diagram', JSON.stringify(saved));

This is why LightDraw works well for AI agents and config-driven UIs: the agent emits JSON; your page only mounts it.

Quick reference: diagram type values

typeKey propsWire flow?
flowchartdata.nodes, data.edgesYes
stateMachinedata.states, data.transitionsYes
processPipelinestages[]Yes
networkTopologydata.nodes, data.edgesYes
canNetworkdata.ecus, data.busLabelYes (virtual bus hops)
classDiagramdata.classes, data.relationsYes
orgChartroot treeNo
electricalSchematiccomponents[]No
mindMapcenter, branchesNo

Try it yourself

  1. npm install lightdraw or drop the CDN script from the top of this post
  2. Copy any scene JSON above into mountDiagram(scene)
  3. Open the live diagram playground: rakeshrajena.github.io/lightDraw/#diagram
  4. Read the flow guide: docs/diagram-flow.md

If this helped, tell me which diagram type you want next (CAN bus walkthrough, mind maps, or exporting animated scenes for docs). Part 1 introduced the library; this part should be enough to ship a real animated diagram from JSON alone.

LightDraw · zero-dependency · JSON-first graphics for the browser

Sunday, 12 July 2026

LightDraw.js: One Library for Dashboards, Automotive HMIs, Diagrams, and UI — Without the Dependency Bloat

Standard

If you've ever tried to ship a real-time dashboard inside an embedded WebView, or let an AI agent generate a live admin panel, you know the pain: React here, Chart.js there, D3 for one chart, a diagram library for topology, and suddenly your "simple HMI" is a 2 MB bundle that won't even run on Chromium 49 in a car infotainment stack.

LightDraw.js is a different bet. It's a zero-dependency, JSON-first 2D graphics engine for the browser — dashboards, automotive clusters, network diagrams, and form-style UI controls, all from one API. Canvas when you need speed. SVG when you need vectors. HTML when you need accessibility or legacy WebView support.

In this guide, I'll walk through what LightDraw actually is, how to get started in under five minutes, and five real-world use cases with code you can copy, tweak, and run today.

What Problem Does LightDraw Solve?

Building interactive 2D graphics in the browser usually means stacking tools:

Typical stackThe pain
React + Chart.js + D3 + diagram libHuge bundle, framework lock-in, four APIs to learn
Raw Canvas APINo scene graph, manual hit-testing, no animation timeline
Low-code / AI UI generatorsOutput is React/JSX — hard to embed in WebView or validate
Automotive HMI toolsExpensive, closed, or not web-native

LightDraw replaces that stack with one engine:

  • Retained-mode scene graph — add, move, and animate nodes; the renderer redraws efficiently
  • Three renderers — Canvas (performance), SVG (vectors), HTML (accessibility + old WebViews)
  • JSON scenes — load, validate, export; ideal for AI agents and config-driven UIs
  • Domain modules — dashboard widgets, automotive cluster, diagrams, UI components — same API
  • ES5 legacy build — ship to Chromium 49+ infotainment without a runtime transpiler

Bundle sizes (gzip): core ~26 KB, full bundle ~101 KB, dashboard plugin ~32 KB, automotive ~29 KB. You only load what you need.

Who Is This For?

✅ Great fit:

  • IoT / ops dashboards on factory tablets or NOC wall displays
  • Automotive digital cockpits fed by CAN or simulator data
  • AI-generated admin panels where the agent outputs JSON, not JSX
  • Network topology and architecture diagrams in docs or incident boards
  • Embedded control panels on Raspberry Pi kiosks — single HTML file, no npm on device

❌ Not the right tool:

  • Full SPA with routing and SSR — use React/Vue for the shell; embed LightDraw in a panel
  • 3D games — use Three.js or Babylon
  • Google Docs–class document editing
  • Native mobile UI — Swift/Kotlin territory

Step 1: Install LightDraw

Option A — npm (modern apps)

npm install lightdraw
import LightDraw from 'lightdraw';

const app = LightDraw.createApp('#app', {
  width: 800,
  height: 600,
  renderer: 'auto',  // canvas | svg | html
  background: '#1e293b'
});

Option B — CDN (no build step)

Perfect for embedded devices, kiosks, or quick prototypes:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lightdraw@1/dist/lightdraw.min.css">
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/lightdraw@1/dist/lightdraw.min.js"></script>
<script>
  const app = LightDraw.createApp('#app', { width: 800, height: 600, renderer: 'html' });
</script>

Expected output after Step 1: An empty canvas (or HTML surface) with your chosen background color. No errors in the console. You're ready to draw.

Step 2: Draw Your First Scene (JavaScript API)

LightDraw uses a retained-mode scene graph. You create nodes, add them to the stage, and the engine handles redraws, hit-testing, and animation.

const app = LightDraw.createApp('#app', {
  width: 800,
  height: 600,
  renderer: 'canvas',
  background: '#1e293b'
});

const circle = app.circle({
  x: 200,
  y: 200,
  radius: 50,
  fill: '#2563eb',
  draggable: true
});

app.add(circle);

circle.on('click', () => {
  circle.animate({
    scale: 1.5,
    duration: 300,
    easing: 'easeOutBounce'
  });
});

Output:

What you'll see: A blue circle on a dark slate background. Click it — it bounces to 1.5× scale. Drag it — it moves with your pointer. That's the scene graph and interaction layer working without you writing a single requestAnimationFrame loop by hand.

Step 3: Build UIs from JSON (The AI-Friendly Path)

This is where LightDraw diverges from most canvas libraries. Instead of imperative drawing calls, you can describe an entire UI as JSON and load it in one line:

app.loadJSON({
  type: 'group',
  children: [
    { type: 'thermometer', props: { value: 72, x: 24, y: 24 } },
    {
      type: 'lineChart',
      props: {
        data: [18, 22, 31, 28, 35],
        width: 400,
        height: 160,
        x: 24,
        y: 120
      }
    },
    { type: 'gauge', props: { value: 68, size: 110, x: 480, y: 40 } },
    {
      type: 'statusBar',
      props: {
        segments: ['Connected', 'MQTT', '1.2k msg/s'],
        x: 24,
        y: 300
      }
    }
  ]
});

Output:

What you'll see: A mini ops dashboard — thermometer at 72°, a line chart with five data points, a gauge at 68%, and a status bar showing connection info. No React. No Chart.js import. One JSON object.

To update live data:

const gauge = app.stage.findOne('gauge');
gauge.set('value', 85);
app.requestRender();

Step 4: Validate JSON Before Rendering (Critical for AI)

When an LLM generates your UI, you must validate before rendering. LightDraw ships schema docs and a built-in validator:

const scene = await fetch('/api/agent/scene.json').then(r => r.json());

const { valid, errors } = LightDraw.validateSceneJSON(scene);

if (!valid) {
  console.error('Scene validation failed:', errors);
  // Example error output:
  // ["Unknown type 'gague' — did you mean 'gauge'?"]
  return;
}

app.clear();
app.loadJSON(scene);
app.setUiTheme({ preset: 'slate', mode: 'dark' });

Expected output when valid: A themed dark UI renders immediately (see AI admin panel example below).

Expected output when invalid: valid: false and a human-readable errors array — no half-broken UI on screen.

The validation pipeline for AI:

User prompt → LLM + schema docs → scene JSON → validateSceneJSON → loadJSON → live UI

Step 5: Pick the Right Renderer

RendererBest forTrade-off
canvas60 FPS animations, 1000+ nodes, automotive clusterRaster — export as PNG
svgCrisp vectors, zoom-friendly diagramsDOM-heavy at very large node counts
htmlForm controls, accessibility, legacy WebViewsNot ideal for 5000-node particle systems
autoLet LightDraw pick based on contextGood default for prototyping

Benchmark snapshot (canvas renderer, 1000 nodes): render ~0.46 ms per frame — enough headroom for 60 FPS on mid-range hardware.


Real-World Use Cases (With Code and Screenshots)

Use Case 1: IoT / Factory Floor Dashboard

Scenario: A tablet on the factory floor shows live sensor readings from MQTT. No React build pipeline on the device.

const app = LightDraw.createApp('#dashboard', {
  width: 1024,
  height: 600,
  renderer: 'canvas'
});

app.loadJSON({
  type: 'group',
  children: [
    { type: 'thermometer', props: { value: 72, x: 24, y: 24, label: 'Line 3 Temp' } },
    { type: 'gauge', props: { value: 68, size: 120, x: 200, y: 24, label: 'Humidity %' } },
    {
      type: 'lineChart',
      props: {
        data: [18, 22, 31, 28, 35, 42, 38],
        width: 500,
        height: 180,
        x: 24,
        y: 180,
        title: 'Throughput (units/hr)'
      }
    },
    {
      type: 'statusBar',
      props: {
        segments: ['● Connected', 'MQTT', '1.2k msg/s'],
        x: 24,
        y: 400
      }
    }
  ]
});

Output:

What operators see: Gauges and charts update in real time. Status bar shows broker health. Entire UI fits in ~101 KB gzip (full bundle via CDN).

Use Case 2: Automotive Digital Cockpit

Scenario: An instrument cluster in an infotainment WebView, updating at 30–60 FPS from CAN bus or a driving simulator.

const app = LightDraw.createApp('#cluster', {
  width: 800,
  height: 480,
  renderer: 'canvas'
});

app.loadJSON({
  type: 'instrumentCluster',
  props: {
    theme: 'classic',
    width: 800,
    height: 480,
    speed: 0,
    rpm: 800,
    fuel: 100
  }
});

const cluster = app.stage.children[0];

function onDriveTick(canData) {
  LightDraw.applyDriveState(cluster, {
    speed: canData.speed,
    rpm: canData.rpm,
    fuel: canData.fuelPercent
  });
  app.requestRender();
}

Output:

What the driver sees: Speedometer at 95, tach at 3200 RPM, fuel at 68%. Smooth updates because render stays sub-millisecond for typical cluster node counts.

Use Case 3: AI-Generated Admin Panel

Scenario: Your internal copilot receives "Build a server health dashboard with CPU chart and acknowledge button." It outputs JSON, not React.

const app = LightDraw.createApp('#app', {
  width: 960,
  height: 540,
  renderer: 'html'
});

const scene = await agentResponse.json();

if (LightDraw.validateSceneJSON(scene).valid) {
  app.loadJSON(scene);
  app.setUiTheme({ preset: 'slate', mode: 'dark' });
}

Example LLM output (scene JSON):

{
  "type": "group",
  "children": [
    { "type": "card", "props": { "title": "Server health", "x": 16, "y": 16, "width": 420, "height": 200 } },
    { "type": "lineChart", "props": { "data": [22,35,28,48,41,55], "x": 32, "y": 56, "width": 380, "height": 140 } },
    { "type": "button", "props": { "label": "Acknowledge", "variant": "primary", "x": 480, "y": 200 } }
  ]
}

Output:

What the user sees: A card titled "Server health", CPU line chart trending upward, and a primary "Acknowledge" button — rendered in seconds, no codegen pipeline.

Use Case 4: Network / Architecture Diagram

Scenario: An SRE pastes an LLM-generated topology into an incident board during an outage.

app.loadJSON({
  type: 'networkTopology',
  props: {
    data: {
      nodes: [
        { id: 'gw', label: 'Gateway', type: 'router', x: 400, y: 40 },
        { id: 'api', label: 'API', type: 'server', x: 200, y: 160 },
        { id: 'db', label: 'Database', type: 'server', x: 400, y: 160 },
        { id: 'cache', label: 'Redis', type: 'server', x: 600, y: 160 }
      ],
      edges: [
        { from: 'gw', to: 'api' },
        { from: 'gw', to: 'db' },
        { from: 'api', to: 'cache' }
      ]
    }
  }
});

Output:

What you see: Router at top, three servers below, connectors auto-routed between nodes — not hand-drawn SVG paths.

Use Case 5: Embedded Control Panel (CDN, Zero Build)

Scenario: A Raspberry Pi kiosk controls industrial pumps. Firmware team delivers one HTML file. No npm on the device.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lightdraw@1/dist/lightdraw.min.css">
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/lightdraw@1/dist/lightdraw.min.js"></script>
<script>
  const app = LightDraw.createApp('#app', { width: 480, height: 320, renderer: 'html' });
  app.loadJSON({
    type: 'group',
    children: [
      { type: 'toggle', props: { label: 'Pump A', value: true, x: 20, y: 20 } },
      { type: 'slider', props: { value: 60, width: 200, x: 20, y: 70, label: 'Flow rate %' } },
      { type: 'button', props: { label: 'Emergency stop', variant: 'danger', x: 20, y: 130 } },
      { type: 'statusBar', props: { segments: ['PLC Online', 'Pump A: RUN'], x: 20, y: 200 } }
    ]
  });
</script>

Output:

What the operator sees: Toggle for Pump A (on), slider at 60%, red emergency stop button, status bar showing PLC state.

Console output on interaction:

Control changed: toggle true
Control changed: slider 73
Control changed: button (click event)

How LightDraw Compares

LightDrawReact + Chart.jsD3.jsGoJS / commercial
Runtime deps0React + chart libD3License + lib
Full HMI in one package❌ assemble yourself❌ data-viz focusPartial
JSON → live UI❌ usually codegenSome import/export
Canvas 60 FPS sceneVia chart canvasManualVaries
ES5 / old WebView✅ legacy build❌ needs buildVaries
AI agent friendly✅ schema docs❌ outputs JSX
LicenseMITMITISCOften paid

LightDraw is not a React replacement. It's a graphics engine you embed inside any page or framework.

Try It Yourself

git clone https://github.com/rakeshrajena/lightDraw.git
cd lightDraw
npm install
npm run build
npm run dev:website   # → http://localhost:5173

Wrapping Up

If your project touches embedded web, real-time dashboards, automotive HMIs, or AI-generated UIs, the usual "assemble five libraries" approach costs you bundle size, validation complexity, and WebView compatibility headaches.

LightDraw gives you one scene graph, three renderers, JSON-first loading with validation, and domain modules that cover the widgets you'd otherwise glue together yourself. Start with a CDN script tag and loadJSON — you can have a working dashboard on screen before your npm install finishes elsewhere.

MIT licensed. Built for embedded web, dashboards, and AI-built UIs.

Sunday, 15 February 2026

WebAssembly (WASM): A Complete Beginner’s Tutorial

Standard

Introduction: What is WASM?

WebAssembly (WASM) is a low-level, binary instruction format designed to run fast, safely, and portably on the web and beyond. It lets you run code written in languages like Rust, C/C++, and Go inside the browser or on servers at near-native speed.

Think of WASM as a universal execution target: you compile your program to WASM once, and it runs consistently across platforms.

Figure 1: WebAssembly enables high-performance code execution in browsers and beyond. This diagram shows how WASM bridges the gap between native performance and web portability.

Key Characteristics

  • Binary format: Compact, efficient, and fast to parse
  • Stack-based virtual machine: Simple execution model
  • Sandboxed: Secure by design, no arbitrary system access
  • Portable: Runs the same way across different platforms
  • Fast: Near-native performance (typically 80-90% of native speed)

What WASM is NOT

  • Not a programming language: You write code in Rust, C++, Go, etc., then compile to WASM
  • Not a replacement for JavaScript: It works alongside JavaScript
  • Not just for the web: Can run on servers, edge computing, IoT devices

Why WASM Was Invented

JavaScript unlocked the web, but it has limits for:

  • CPU-heavy workloads: Image/video processing, physics simulations, cryptography
  • Large codebases: Games, CAD software, compilers
  • Predictable performance: JavaScript’s JIT compilation can be unpredictable
  • Memory control: Limited ability to manage memory efficiently

WASM solves this by:

  • Providing a compact binary format (faster load/parse than JavaScript)
  • Enabling near-native execution speed (predictable performance)
  • Running in a secure sandbox (better security model)
  • Working alongside JavaScript, not replacing it (seamless integration)

The Performance Gap

Before WASM, developers had to choose between:

  • JavaScript: Easy to use, but slower for compute-intensive tasks
  • Native plugins: Fast, but insecure and platform-specific
  • Server-side processing: Secure, but adds latency and server costs

WASM provides the best of all worlds: JavaScript’s ease of use, native performance, and web security.

Understanding the WASM Architecture

Figure 2: The WASM compilation pipeline. Source code in languages like Rust, C++, or Go is compiled to WASM binary format, which can then be executed in browsers or WASM runtimes.

Core Components

WASM Module

  • The compiled binary (.wasm file)
  • Contains functions, memory, tables, and imports/exports
  • Loaded once and can be instantiated multiple times

WASM Engine:

  •  Executes the module

    In browsers: V8 (Chrome), SpiderMonkey (Firefox), JavaScriptCore (Safari)
  • Standalone: Wasmtime, Wasmer, WAVM

Host Environment

  • JavaScript (or other host language)

    Loads and instantiates WASM modules
  • Provides imports (functions, memory) to WASM
  • Calls exported functions from WASM

Linear Memory

  • A contiguous array of bytes

    Shared between WASM and JavaScript
  • Accessed via typed arrays (Uint8Array, Int32Array, etc.)

Sandbox

  • Security boundary

    No direct file system access
  • No network access (unless provided by host)
  • No arbitrary system calls
  • How WASM Works: The Complete Flow

Figure 3: V8’s WASM compilation pipeline. This shows how WASM binaries are validated, decoded, compiled, and optimized before execution.


Figure 4: Detailed view of the WASM compilation and execution pipeline, showing the stages from binary loading to optimized execution.

Step-by-Step Execution Flow

1. Source Code: Write code in Rust, C++, Go, or another supported language
2. Compilation: Compiler (rustc, emcc, go compiler) generates .wasm binary

# Example: Rust to WASM
wasm-pack build --target web

3. Loading: JavaScript loads the WASM module

const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('module.wasm')
);

4. Validation

  • WASM engine validates the binary
  • Checks instruction validity
  • Verifies type safety
  • Ensures memory safety

5. Compilation:

  • Engine compiles WASM to native code

    JIT (Just-In-Time): Compiles during execution
  • AOT (Ahead-Of-Time): Pre-compiles for faster startup
6. Execution
  • Native code runs at near-native speed

7. Interoperation

  • JavaScript and WASM call each other

    JavaScript calls WASM functions
  • WASM calls JavaScript functions (via imports)

  1. Getting Started: Your First WASM Project

Let’s create your first WASM project step by step. We’ll use Rust as it has excellent WASM tooling.

Prerequisites

  1. Install Rust: Visit rustup.rs
  2. Install wasm-packcargo install wasm-pack
  3. A modern browser: Chrome, Firefox, Safari, or Edge

Step 1: Create a New Rust Project

cargo new --lib wasm-hello
cd wasm-hello

Step 2: Configure Cargo.toml

Edit Cargo.toml:

[package]
name = "wasm-hello"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Step 3: Write Your First WASM Function

Edit src/lib.rs:

use wasm_bindgen::prelude::*;

// Import the `console.log` function from JavaScript
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

// Define a macro to make console.log easier to use
macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

// Export a simple function
#[wasm_bindgen]
pub fn greet(name: &str) {
    console_log!("Hello, {}! Welcome to WebAssembly!", name);
}

// Export a function that returns a value
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Export a function that processes arrays
#[wasm_bindgen]
pub fn sum_array(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

Step 4: Build the WASM Module

wasm-pack build --target web

This creates a pkg/ directory with:

  • wasm_hello_bg.wasm: The compiled WASM binary
  • wasm_hello.js: JavaScript bindings
  • wasm_hello.d.ts: TypeScript definitions

Step 5: Create an HTML File

Create index.html:

<!DOCTYPE html>
<html>
<head>
    <title>WASM Hello World</title>
</head>
<body>
    <h1>WebAssembly Demo</h1>
    <button id="greet-btn">Greet</button>
    <button id="add-btn">Add Numbers</button>
    <button id="sum-btn">Sum Array</button>
    <div id="output"></div>

    <script type="module">
        import init, { greet, add, sum_array } from './pkg/wasm_hello.js';
        
        async function run() {
            // Initialize the WASM module
            await init();
            
            const output = document.getElementById('output');
            
            // Greet button
            document.getElementById('greet-btn').addEventListener('click', () => {
                greet('WebAssembly Developer');
            });
            
            // Add button
            document.getElementById('add-btn').addEventListener('click', () => {
                const result = add(42, 17);
                output.textContent = `42 + 17 = ${result}`;
            });
            
            // Sum array button
            document.getElementById('sum-btn').addEventListener('click', () => {
                const numbers = [1, 2, 3, 4, 5, 10, 20];
                const result = sum_array(numbers);
                output.textContent = `Sum of [1,2,3,4,5,10,20] = ${result}`;
            });
        }
        
        run();
    </script>
</body>
</html>

Step 6: Serve and Test

You need a local server (WASM requires HTTP, not file://):

# Using Python
python3 -m http.server 8000

# Using Node.js (if you have http-server installed)
npx http-server

# Using Rust (if you have it installed)
cargo install basic-http-server
basic-http-server

Open http://localhost:8000 in your browser and click the buttons!

Languages You Can Use with WASM

Rust (Recommended for Beginners)

Pros:

  • Excellent tooling (wasm-packwasm-bindgen)
  • Memory safety without garbage collection
  • Great performance
  • Active community

Cons:

  • Steeper learning curve
  • Compile times can be slow

Best for: New projects, performance-critical code

C/C++

Pros:

  • Mature ecosystem (Emscripten toolchain)
  • Great for porting existing codebases
  • Maximum performance

Cons:

  • More complex setup
  • Manual memory management
  • Larger binaries

Best for: Porting existing C/C++ libraries

Go

Pros:

  • Simple syntax
  • Built-in WASM support
  • Easy to learn

Cons:

  • Larger runtime (includes garbage collector)
  • Slower than Rust/C++
  • Less control over memory

Best for: Simple projects, rapid prototyping

AssemblyScript

Pros:

  • TypeScript-like syntax
  • Familiar to web developers
  • Small binaries

Cons:

  • Less mature than Rust/C++
  • Limited ecosystem

Best for: Web developers familiar with TypeScript

Zig

Pros:

  • Modern systems language
  • Small binaries
  • Good performance

Cons:

  • Still emerging
  • Smaller community

Best for: Systems programming, experimental projects

WASM Language Comparison: Complexity vs Speed

LanguageSetup ComplexityRuntime SizePerformanceBest Use
RustMediumSmall⭐⭐⭐⭐⭐New high-perf code
C/C++HighMedium⭐⭐⭐⭐⭐Porting native libs
GoLow–MediumLarge⭐⭐⭐Simplicity, tooling
AssemblyScriptLowSmall⭐⭐⭐Web devs
ZigMediumSmall⭐⭐⭐⭐Systems work

WASM Memory Management

WASM uses a linear memory model: a single, contiguous array of bytes that can grow.

Understanding Linear Memory

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

#[wasm_bindgen]
pub fn process_buffer(buffer: &[u8]) -> Vec<u8> {
    // Process the buffer (e.g., apply a filter)
    buffer.iter().map(|&x| x.wrapping_add(10)).collect()
}

#[wasm_bindgen]
pub fn allocate_buffer(size: usize) -> *mut u8 {
    let mut buffer = vec![0u8; size];
    let ptr = buffer.as_mut_ptr();
    std::mem::forget(buffer); // Prevent deallocation
    ptr
}

#[wasm_bindgen]
pub fn free_buffer(ptr: *mut u8, size: usize) {
    unsafe {
        let _ = Vec::from_raw_parts(ptr, size, size);
    }
}

Memory Sharing with JavaScript

// JavaScript side
const wasmModule = await WebAssembly.instantiateStreaming(
    fetch('module.wasm')
);

// Access WASM memory
const memory = wasmModule.instance.exports.memory;
const memoryView = new Uint8Array(memory.buffer);

// Write data to WASM memory
memoryView[0] = 42;
memoryView[1] = 24;

// Call WASM function that processes the memory
wasmModule.instance.exports.process_memory();

Best Practices for Memory

  1. Reuse buffers: Don’t allocate/deallocate frequently
  2. Use typed arrays: More efficient than regular arrays
  3. Monitor memory growth: Use memory.grow() carefully
  4. Free allocated memory: Prevent memory leaks

JavaScript Interoperability

WASM and JavaScript work together seamlessly. Here’s how:

Calling WASM Functions from JavaScript

// Rust/WASM
#[wasm_bindgen]
pub fn calculate_fibonacci(n: u32) -> u64 {
    if n <= 1 {
        return n as u64;
    }
    let mut a = 0u64;
    let mut b = 1u64;
    for _ in 2..=n {
        let temp = a + b;
        a = b;
        b = temp;
    }
    b
}
// JavaScript
import init, { calculate_fibonacci } from './pkg/module.js';

await init();
const result = calculate_fibonacci(40);
console.log(`Fibonacci(40) = ${result}`);

Calling JavaScript Functions from WASM

// Rust/WASM
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    // Call JavaScript's console.log
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
    
    // Call a custom JavaScript function
    #[wasm_bindgen(js_name = "customFunction")]
    fn custom_function(value: i32);
}

#[wasm_bindgen]
pub fn wasm_function() {
    log("Hello from WASM!");
    custom_function(42);
}
// JavaScript
function customFunction(value) {
    console.log(`Received from WASM: ${value}`);
}

// Make it available globally
window.customFunction = customFunction;

Passing Complex Data

// Rust - Using serde for JSON
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Person {
    name: String,
    age: u32,
}

#[wasm_bindgen]
pub fn create_person(name: String, age: u32) -> JsValue {
    let person = Person { name, age };
    JsValue::from_serde(&person).unwrap()
}
// JavaScript
const person = create_person("Alice", 30);
console.log(person); // { name: "Alice", age: 30 }

Real-World Examples

Example 1: Image Processing

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale_image(pixels: &mut [u8]) {
    // Process every 4 bytes (RGBA)
    for chunk in pixels.chunks_exact_mut(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        
        // Grayscale formula
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        
        chunk[0] = gray; // R
        chunk[1] = gray; // G
        chunk[2] = gray; // B
        // chunk[3] stays as alpha
    }
}

Example 2: Mathematical Computation

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn matrix_multiply(
    a: &[f64],
    b: &[f64],
    n: usize
) -> Vec<f64> {
    let mut result = vec![0.0; n * n];
    
    for i in 0..n {
        for j in 0..n {
            for k in 0..n {
                result[i * n + j] += a[i * n + k] * b[k * n + j];
            }
        }
    }
    
    result
}

Example 3: String Processing

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

#[wasm_bindgen]
pub fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

Browser Support and Deployment

Browser Support

All modern browsers support WASM:

  • Google Chrome: v57+ (March 2017)
  • Mozilla Firefox: v52+ (March 2017)
  • Apple Safari: v11+ (September 2017)
  • Microsoft Edge: v16+ (October 2017)

Support includes:

  • Core WASM 1.0
  • Streaming compilation
  • Threads (experimental)
  • SIMD (experimental)
  • Reference types
  • Tail calls

Deployment Best Practices

1. Compress WASM files: Use gzip or brotli compression

# Server should compress .wasm files
# Nginx example:
location ~* \.wasm$ {
    gzip on;
    gzip_types application/wasm;
}

2. Use streaming compilation: Faster startup

// Good: Streaming
const module = await WebAssembly.instantiateStreaming(
    fetch('module.wasm')
);

// Avoid: Non-streaming
const bytes = await fetch('module.wasm').then(r => r.arrayBuffer());
const module = await WebAssembly.instantiate(bytes);

3. Lazy load: Only load WASM when needed

async function loadWasmWhenNeeded() {
    if (!wasmModule) {
        wasmModule = await import('./pkg/module.js');
        await wasmModule.default();
    }
    return wasmModule;
}

4. Cache WASM modules: Use service workers or HTTP caching

Performance Considerations

When WASM Outperforms JavaScript

  • CPU-intensive tasks: Image processing, cryptography, physics
  • Large loops: Mathematical computations
  • Memory-intensive operations: Large array manipulations
  • Predictable workloads: Consistent performance matters

When JavaScript is Fine

  • DOM manipulation: JavaScript is optimized for this
  • Small scripts: Overhead of WASM not worth it
  • Rapid prototyping: JavaScript is faster to write
  • Simple logic: No performance benefit

Performance Tips

1. Minimize JS ↔ WASM calls: Batch operations

// Bad: Many small calls
for item in items {
    process_item(item); // Called from JS
}

// Good: One call with batch
process_batch(items); // Single call
2. Use typed arrays: Faster than regular arrays
3. Avoid unnecessary allocations: Reuse buffers
4. Profile your code: Use browser dev tools

Common Use Cases

1. Image and Video Processing

  • Figma: Real-time graphics rendering
  • FFmpeg.wasm: Video/audio processing in browser
  • Image filters: Instagram-like effects

2. Games and Simulations

  • Unity WebGL: Game engines
  • Physics engines: Real-time simulations
  • 3D graphics: WebGL acceleration

3. Data Processing

  • SQLite WASM: Embedded database
  • CSV parsing: Large file processing
  • Data compression: Client-side compression

4. Cryptography

  • Encryption/Decryption: Client-side security
  • Hashing: Password hashing
  • Digital signatures: Cryptographic operations

5. Scientific Computing

  • Numerical analysis: Complex calculations
  • Machine learning inference: Running ML models
  • Simulations: Scientific modeling

6. Compilers and Interpreters

  • Language runtimes: Python, Lua in browser
  • Code transpilation: Source-to-source compilation
  • Virtual machines: Custom VMs

Limitations and When Not to Use WASM

Limitations

1. No direct DOM access: Must go through JavaScript

// This doesn't exist in pure WASM
// document.getElementById("myDiv") // ❌

// You need JavaScript bridge
#[wasm_bindgen]
extern "C" {
    fn get_element_by_id(id: &str) -> JsValue;
}
2. Debugging challenges: Harder than JavaScript debugging
3. Binary size: Can be larger than JS for small tasks
4. Startup overhead: Module loading and compilation time
5. Limited garbage collection: Manual memory management in some languages

When NOT to Use WASM

  • Simple UI logic: JavaScript is better
  • Small scripts: Overhead not worth it
  • Rapid prototyping: JavaScript is faster to develop
  • DOM-heavy applications: JavaScript is optimized for this
  • Simple calculations: No performance benefit

When to Use WASM

✅ DO use WASM for:

  • CPU-intensive computations
  • Porting existing C/C++/Rust codebases
  • Performance-critical code
  • Large codebases that benefit from compilation
  • Cross-platform consistency

❌ DON’T use WASM for:

  • Simple DOM manipulation
  • Small utility functions
  • Rapid prototyping
  • Code that’s already fast enough in JavaScript

Best Practices

1. Start Small

Begin with simple functions and gradually add complexity.

2. Profile First

Don’t assume WASM is faster. Measure:

console.time('js-version');
// JavaScript code
console.timeEnd('js-version');

console.time('wasm-version');
// WASM code
console.timeEnd('wasm-version');

3. Error Handling

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn safe_divide(a: f64, b: f64) -> Result<f64, JsValue> {
    if b == 0.0 {
        Err(JsValue::from_str("Division by zero"))
    } else {
        Ok(a / b)
    }
}

4. Type Safety

Use TypeScript definitions generated by wasm-pack:

// Generated .d.ts file
export function add(a: number, b: number): number;

5. Testing

Test WASM modules thoroughly:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

6. Documentation

Document your WASM functions:

/// Adds two numbers together.
/// 
/// # Arguments
/// 
/// * `a` - First number
/// * `b` - Second number
/// 
/// # Returns
/// 
/// The sum of `a` and `b`
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Conclusion

WebAssembly is a powerful technology that extends what’s possible on the web. It doesn’t replace JavaScript, it complements it by enabling high-performance code execution where needed.

Key Takeaways

  1. WASM is for performance: Use it when JavaScript isn’t fast enough
  2. Works alongside JavaScript: They complement each other
  3. Multiple language support: Choose based on your needs
  4. Secure and portable: Runs safely across platforms
  5. Growing ecosystem: More tools and libraries every day

Next Steps

  • Build your first WASM project
  • Explore different languages (Rust, Go, C++)
  • Profile and optimize your code
  • Deploy to production
  • Contribute to the WASM ecosystem

Bibilography

Remember & Please Note: WASM is a tool, not a silver bullet. Use it where it makes sense, and JavaScript where it doesn’t. The best applications use both together!