DEV Community

A 100% client-side toolbox: shipping 17 calculators with no backend

Doing video work in the browser sounds like a compromise - until you realize the alternative is uploading proprietary footage and delivery specs to someone else's server just to multiply a few numbers. PostMicroTools does bitrate math, storage planning, LUT previews, and delivery-spec validation entirely in your tab, and never sends a byte anywhere.

Why client-side first

For post-production utilities, "no backend" is the right call on three axes at once.

  • Privacy. A colorist checking whether a deliverable meets a streamer's spec, or an editor estimating card budget for a shoot under NDA, should not have to ship file names, codecs, or resolutions to a third party. If the math happens in the browser, there is nothing to log, breach, or subpoena.
  • Cost. Static files on a CDN cost effectively nothing to serve and scale to any traffic without a single server process to keep alive. No database, no autoscaling, no 2 a.m. pager.
  • UX. No round trip means results land as you type. A bitrate estimate updates on every keystroke because it is a function call, not a fetch.

Architecture: a multi-tool hub as static files

Each tool is an isolated entry point that shares a small set of pure utilities. The build emits one chunk per tool plus a common vendor chunk, so visiting the bitrate calculator never loads the LUT viewer's WebGL code.

/tools
  bitrate/
    index.html
    bitrate.ts
  storage/
    index.html
    storage.ts
  lut-preview/
    index.html
    lut.ts
/shared
  units.ts       // GB/GiB, Mbps/MB-s conversions
  codecs.ts      // codec profile data
  format.ts      // number + duration formatting

Routing is just the filesystem. Each tool is its own HTML page, the hub page is a static index linking to them, and the shared utilities are tree-shaken into whichever tools import them. No client-side router, no hydration waterfall.

A worked example: the bitrate calculator

The interesting decision here is encoding camera profiles as data, not code. REDCODE, ARRIRAW, BRAW, and ProRes RAW all express their target as a data rate that scales with resolution and frame rate - so each profile is a row, and one formula consumes all of them.

// codecs.ts - profiles as data
export const RAW_PROFILES = {
  "redcode-8-1":    { label: "REDCODE 8:1",    mbpsPerMP: 3.2 },
  "arriraw-uhd":    { label: "ARRIRAW UHD",    mbpsPerMP: 12.0 },
  "braw-5-1":       { label: "BRAW 5:1",       mbpsPerMP: 2.6 },
  "prores-raw-hq":  { label: "ProRes RAW HQ",  mbpsPerMP: 9.4 },
} as const;

// bitrate.ts - one formula for every profile
function bitrateMbps(p, widthPx, heightPx, fps) {
  const megapixels = (widthPx * heightPx) / 1_000_000;
  return p.mbpsPerMP * megapixels * (fps / 24);
}

function fileSizeGB(mbps, seconds) {
  return (mbps * seconds) / 8 / 1000; // Mbit -> MByte -> GB
}

Adding a new codec is a one-line addition to the data object. No new branches, no new UI code - the dropdown is generated from Object.entries(RAW_PROFILES).

Real work in the browser: parsing a .cube LUT

LUT preview is where "client-side" stops being a slogan. A .cube file is plain text: a LUT_3D_SIZE line followed by Nยณ RGB triples. Parsing it is trivial, and the result goes straight into a WebGL 3D texture so the GPU does the interpolation.

function parseCube(text: string) {
  let size = 0;
  const data: number[] = [];
  for (const raw of text.split("\n")) {
    const line = raw.trim();
    if (!line || line.startsWith("#")) continue;
    if (line.startsWith("LUT_3D_SIZE")) {
      size = parseInt(line.split(/\s+/)[1], 10);
      continue;
    }
    const m = line.split(/\s+/).map(Number);
    if (m.length === 3 && m.every((n) => !Number.isNaN(n))) {
      data.push(m[0], m[1], m[2]);
    }
  }
  return { size, data: new Float32Array(data) };
}

The parsed cube becomes a gl.TEXTURE_3D, the frame is drawn to a <canvas>, and a fragment shader samples the LUT per pixel. A drag slider just clips the LUT-applied half against the original - a before/after wipe at 60fps, with the source image never leaving the page.

Keeping 17 tools fast

The whole point collapses if the hub ships a 2 MB bundle on first paint. Three rules keep it honest:

  • No framework tax. Tools are vanilla TS with small DOM helpers. There is no virtual DOM to ship for a page that renders one form.
  • Code-split per tool. Each entry point is its own chunk; the bitrate page is a few KB of JS.
  • Lazy-load the heavy ones. WebGL and the LUT parser load only when that tool mounts, behind a dynamic import(). The 16 tools you did not open cost you nothing.

The result is a hub where any single tool is interactive almost immediately, regardless of how many siblings exist.

Try it

It is free, there is no signup, and nothing you type or drop in is ever uploaded - every calculation runs in your own browser tab: PostMicroTools.

Built by Furiosa Studio.

Comments

No comments yet. Start the discussion.