{"slug": "show-hn-noopjs-a-compiler-that-ships-0-kb-javascript-for-static-pages", "title": "Show HN: NoopJS – A compiler that ships 0 KB JavaScript for static pages", "summary": "NoopJS, a new compiler framework, ships 0 KB of JavaScript for static pages by compiling JSX to vanilla DOM, eliminating framework runtime code. The framework delivers a 0.06s Largest Contentful Paint on server-side rendered pages and reduces client runtime to as little as 466 bytes for interactive components, addressing Core Web Vitals and bundle size concerns.", "body_md": "[Quick Start](#quick-start) ·\n[Live Demo](/noop-js/noopjs/blob/main/examples/blog) ·\n[Documentation](/noop-js/noopjs/blob/main/docs/index.html) ·\n[Roadmap](#roadmap)\n\n```\n    ╔══════════════════════════════════════════════════════════╗\n    ║  .noop.tsx   ──►  Compiler  ──►  Vanilla JS            ║\n    ║                    │                                     ║\n    ║                    ├── SSR:  HTML + Serialized State     ║\n    ║                    │         │                           ║\n    ║                    │         ▼                           ║\n    ║                    │    Client Runtime  ◄── 0–3.7 KB     ║\n    ║                    │    (No hydration — just resume)     ║\n    ║                    │                                     ║\n    ║                    └── Atomic CSS  +  Tailwind v4        ║\n    ║                        (Zero runtime CSS-in-JS)         ║\n    ╚══════════════════════════════════════════════════════════╝\n```\n\n**AI generates code now.** Frameworks built on hooks, rules, and runtime contracts (React, Vue, Angular) were designed for humans. An AI doesn't need rules — it needs a framework that compiles away. NoopJS components are plain functions. No `useMemo`\n\n, no `useCallback`\n\n, no rules of hooks. The compiler handles everything.\n\n**Performance is no longer optional.** Core Web Vitals are SEO signals. NoopJS delivers a 0.06s LCP on an SSR page — not in a benchmark, but in a real blog application with Tailwind CSS.\n\n**The silos must break.** Components should work everywhere. NoopJS components compile to native Custom Elements on demand. Write once, embed anywhere.\n\n**JavaScript bundles must shrink.** The average React page ships ~45 KB of framework JS. NoopJS ships **0 KB for static pages**, **466 B for resume** (counter fully interactive — signal, binding, handler), and **317 B inline + 3.5 KB cached shared runtime for SPA**.\n\n```\nnpm install @noopjs/vite        # or scaffold: npm create noopjs\n```\n\nThen add one plugin to `vite.config.ts`\n\n:\n\n``` js\nimport { noopVite } from '@noopjs/vite';\n\nexport default defineConfig({\n  plugins: [noopVite()],\n});\n```\n\n| What you get | Why it matters |\n|---|---|\nCompiler |\nJSX → vanilla DOM. No framework code ships. |\nSignals |\nTC39-standard `signal` , `computed` , `effect` , `batch` . |\nAtomic CSS |\nStyle objects → hashed utility classes. Zero runtime CSS-in-JS. |\nSSR engine |\nRender to HTML, serialize state, resume on client. True resumability. |\nClient runtime |\n0 KB static, 466 B resume (inline), 317 B inline + 3.5 KB cached shared for SPA. Re-attaches signals to DOM without re-running components. Native `<link rel=\"prefetch\">` eliminates JS prefetcher. |\nSPA router |\nIntercepts `<a>` clicks. View Transitions API. Native `<link rel=\"prefetch\">` . |\nEvent delegation |\nSingle global listener. Handlers loaded lazily on first interaction. |\nSPA security |\nmXSS-immune page swaps via per-render sentinel manifest. ~50 bytes. No DOMPurify. |\nTailwind v4 |\nFirst-class token resolver. `token.spacing[6]` → `p-6` . |\nCustom Elements |\nExport as native Web Components via `@noopjs customElement` directive. |\nForm helpers |\n`useField()` , `<Form>` , validation — property-assigned, not `setAttribute` . ~100 lines. |\nDevTools |\nRuntime bridge instruments all `signal()` /`effect()` calls. Floating panel (Cmd+Shift+N). |\nStreaming SSR |\n`renderToStream()` emits content-first HTML. Chunked transfer encoding. |\nESLint plugin |\n4 rules: module-level signals, dynamic DOM in resume, missing key, dangerous innerHTML. |\nCLI |\n`dev` , `build` , `generate` , `analyze` , `check` , `init` . |\nHMR |\nFull hot module replacement in development. |\n\n```\nnpm create noopjs               # any tool: pnpm create, yarn create, bun create\n# Pick a template: counter, blog, or empty\n```\n\nOr add to any existing Vite project:\n\n```\nnpm install @noopjs/vite        # or: pnpm add, yarn add, bun add\njs\n// vite.config.ts\nimport { noopVite } from '@noopjs/vite';\nexport default defineConfig({ plugins: [noopVite()] });\njs\n// src/counter.noop.tsx\nimport { signal } from '@noopjs/signals';\n\nexport const styles = {\n  count: { fontSize: '24px', fontWeight: 'bold' },\n  button: { padding: '8px 16px', cursor: 'pointer' },\n};\n\nexport default function Counter() {\n  const count = signal(0);\n  return (\n    <div>\n      <p className={styles.count}>{count}</p>\n      <button className={styles.button} onClick={() => count.set(count.get() + 1)}>\n        Increment\n      </button>\n    </div>\n  );\n}\npython\n// src/main.ts\nimport Counter from './counter.noop';\ndocument.getElementById('root')!.appendChild(Counter());\nnpx vite              # dev server\nnpx noopjs build      # production build\n```\n\nFine-grained reactivity following the TC39 proposal.\n\n``` js\nimport { signal, computed, effect, batch } from '@noopjs/signals';\n\nconst count = signal(0);\nconst doubled = computed(() => count.get() * 2);\n\neffect(() => console.log(count.get()));\n\nbatch(() => {\n  count.set(1);\n  count.set(2);\n}); // effect runs once\n```\n\nThe compiler transforms `.noop.tsx`\n\ninto vanilla JavaScript at build time.\n\n```\n// You write:\nexport default function Greeting(props: { name: string }) {\n  return <div>Hello, {props.name}!</div>;\n}\n// The compiler generates (simplified):\nexport default function Greeting(props) {\n  const el = document.createElement('div');\n  const txt = document.createTextNode('Hello, ' + props.name);\n  el.appendChild(txt);\n  return el;\n}\n```\n\nNo JSX at runtime. No framework imports. No VDOM. Just DOM.\n\nA minimal form library built on signals. Property-assigned values (not `setAttribute`\n\n) — no friction with `<input value={...}>`\n\n.\n\n``` js\nimport { Form, useField } from '@noopjs/forms';\n\nexport default function SearchPage() {\n  const query = useField('', {\n    validate: (v) => v.length > 0 ? null : 'Enter a query',\n  });\n  const results = signal([]);\n  const loading = signal(false);\n\n  function doSearch() {\n    if (query.validate()) return;\n    loading.set(true);\n    fetch('/api/search?q=' + encodeURIComponent(query.value.get()))\n      .then(r => r.json())\n      .then(r => { results.set(r); loading.set(false); });\n  }\n\n  return (\n    <div>\n      <Form onSubmit={doSearch}>\n        <input\n          {...query.props}\n          onInput={function(e: Event) {\n            query.value.set((e.target as HTMLInputElement).value);\n          }}\n          placeholder=\"Search...\"\n        />\n        {query.error.get() && <span>{query.error.get()}</span>}\n        <button>Search</button>\n      </Form>\n      {loading.get() && <p>Loading...</p>}\n    </div>\n  );\n}\n```\n\nReturned by `useField` |\nPurpose |\n|---|---|\n`value` |\n`Signal<string>` — read with `field.value.get()` |\n`error` |\n`Signal<string | null>` — validation error, set by `field.validate()` |\n`props` |\n`{ value }` — spread onto `<input>` , `<textarea>` , `<select>` |\n`set(v)` |\nProgrammatically set the value |\n`validate()` |\nRun the validation function, set `error` signal, return error or null |\n`reset()` |\nRestore initial value and clear error |\n\n`onInput`\n\nis defined inline on the `<input>`\n\nelement, not inside `props`\n\n. The compiler detects `onInput={...}`\n\nand binds it via event delegation.\n\n`<Form>`\n\ncreates a `<form>`\n\nelement with `preventDefault`\n\non submit. Works in `client: spa`\n\npages.\n\nHydration runs a component on the client and diffs its output against server HTML — duplicate work. Resumption serializes the reactive graph and re-attaches it without running a single line of component code.\n\nPer-page JavaScript payloads: **0 KB** for `client: none`\n\n, **466 B** gzipped for `client: resume`\n\n(polyfill + signals + bindings + inline handlers), **317 B** inline + **3.5 KB** cached shared runtime for `client: spa`\n\n.\n\nA `// client:`\n\ndirective at the top of a `.noop.tsx`\n\nfile selects how much JS ships to the browser:\n\n| Level | JS Payload | When to Use | Limits |\n|---|---|---|---|\n`none` |\n0 KB | Static content (about, 404, docs) | No interactivity at all |\n`resume` |\n~500 B inline | Forms with validation, toggle buttons, counters | Fixed DOM structure only. Signals and handlers are re-bound without re-running the component. Cannot create or remove DOM nodes dynamically. Use only when the HTML structure is known at SSR time. |\n`spa` |\n~550 B + 3.5 KB cached shared runtime | Dynamic lists, search results, async data, comment threads | Ships a shared router. The component function re-runs on signal changes, so dynamic content (`.map()` , conditionals) updates correctly. |\n`full` |\nsame as spa | Everything | Currently the same as `spa` . Reserved for future use with fully client-rendered pages. |\n\n**Key guidance:** If your page has content that changes based on user interaction (search results, filtered lists, toggled sections), use `client: spa`\n\n. The `resume`\n\nlevel is best for forms, toggles, and counters where the DOM structure is fixed and only values change.\n\n```\n         SSR                               Client\n  ┌─────────────────┐              ┌──────────────────┐\n  │                 │              │                  │\n  │  signal(0)      │  ──state──►  │  signal(0)       │\n  │  effect → DOM   │              │  effect → same   │\n  │                 │   ◄─lazy──   │  DOM (no re-run) │\n  │  handler click  │              │                  │\n  └─────────────────┘              └──────────────────┘\n```\n\n**NoopCSS** — Static style objects extract to atomic CSS classes at build time:\n\n``` js\nexport const styles = {\n  card: { padding: '16px', borderRadius: '8px', display: 'flex', gap: '12px' },\n};\n// className={styles.card} → className=\"_a3f8b2 _c9e12a _e7d4b1\"\n```\n\n**Tailwind v4** — First-class integration via token resolvers:\n\n``` js\nimport { token } from '@noopjs/runtime';\n\n<div style={{ padding: token.spacing[6], color: token.color.blue[500] }}>\n```\n\nResolves to `p-6`\n\n, `text-blue-500`\n\nat compile time. Both systems coexist on the same element:\n\n```\n<div class=\"p-6 _a3f8b2\">Tailwind + NoopCSS</div>\n```\n\nEvery other SPA router that uses `innerHTML`\n\nto swap pages is vulnerable to mutation XSS (mXSS) — parser-confusion attacks that bypass denylist sanitizers like DOMPurify. NoopJS takes a different approach.\n\nWhen the SSR engine renders a component tree, it tags every element with a sequential `data-n`\n\nID and records `{tag, attrs}`\n\ninto a manifest that travels with the serialized state. On the client, before injecting the HTML, the verifier walks the parsed DOM and asks one question of each element: **\"did the SSR engine emit you?\"**\n\nIf the element lacks a `data-n`\n\nmatching the manifest, it's removed. If its tag doesn't match, it's removed. If it carries attributes the SSR didn't emit, they're stripped. This isn't a denylist — it's a provenance check. An attacker cannot forge a valid `data-n`\n\nvalue because they don't control the SSR engine, and they cannot inject elements that survive the verification pass, because the browser's `innerHTML`\n\nparser cannot manufacture a valid sentinel.\n\n**Result: provably mXSS-immune.** All 18 known mXSS payloads blocked, 0 bypasses. The verifier is ~50 bytes gzipped — zero dependencies, no DOMPurify overhead. This is only possible because NoopJS controls both the SSR engine and the client runtime — the same vertical integration that enables resumability.\n\n| Package | Version | Description |\n|---|---|---|\n`@noopjs/signals` |\n1.1.0 | TC39 Signals — `signal` , `computed` , `effect` , `batch` , `untrack` , `readonly` |\n`@noopjs/compiler` |\n1.1.0 | Compiles `.noop.tsx` to vanilla JS. Exports `createTailwindResolver` . |\n`@noopjs/runtime` |\n1.1.0 | Browser runtime — `bindText` , `bindEvent` , `bindStyle` , `onMount` , Context, Portals, Suspense |\n`@noopjs/client` |\n1.1.0 | Client resumer — SSR hydration, SPA router, native prefetch |\n`@noopjs/server` |\n1.1.0 | SSR engine — `renderToString` , `renderToStream` , file-based routing, caching |\n`@noopjs/vite` |\n1.1.0 | Vite plugin — compiles `.noop.tsx` , extracts CSS, HMR, handler splitting |\n`@noopjs/eslint-plugin` |\n1.0.0 | ESLint plugin — 4 rules: no module-level signals, no dynamic DOM in resume, require key in .map(), warn on dangerous innerHTML |\n`@noopjs/css` |\n1.1.0 | Atomic CSS extractor — `extractStyles()` converts style objects to atomic classes |\n`@noopjs/cli` |\n1.1.0 | CLI — `dev` , `build` , `generate` , `analyze` , `check` , `init` |\n`create-noopjs` |\n1.1.0 | `npm create noopjs` — project scaffolding with templates |\n\n```\ncd examples/counter       # Minimal interactive component\nnpm run dev               # Client-rendered, signals + atomic CSS\n\ncd examples/blog          # Full SSR blog with Tailwind\nnpm run ssr               # → http://localhost:3000 (0.06s LCP)\n```\n\nThe blog example demonstrates SSR + Tailwind v4 + NoopCSS + SPA navigation + lazy handler loading end-to-end.\n\n- ✅ Signals (TC39 proposal)\n- ✅ Compiler (JSX → DOM)\n- ✅ NoopCSS (atomic extraction)\n- ✅ Tailwind v4 integration\n- ✅ SSR + resumability\n- ✅ SPA router\n- ✅ Custom Elements export\n- ✅ HMR / dev server\n- ✅ CLI\n\n- ✅ DevTools (runtime bridge + floating panel)\n- ✅ Streaming SSR (\n`renderToStream`\n\n, chunked encoding) - ✅ Form helpers (\n`useField`\n\n,`<Form>`\n\n, validation) - ✅ ESLint plugin (4 rules: module-level signals, dynamic DOM in resume, missing key, dangerous innerHTML)\n- 🔲 Design-system libraries\n- 🔲 Image optimization\n- 🔲 i18n / l10n primitives\n- 🔲 Performance budgets tooling\n- 🔲 ESLint plugin\n\n- 🔲 Noop Cloud (serverless edge SSR)\n- 🔲 AI scaffolding\n- 🔲 Component marketplace\n- 🔲 First-class mobile\n\nNoopJS is open-source. Contributions of all kinds welcome — code, docs, bug reports, ideas.\n\n[Open an issue](https://github.com/noop-js/noopjs/issues)- Submit a PR\n- Improve the documentation in\n`docs/`\n\n- Build example projects\n\n**Mohammed Boukaba** — Creator and lead developer.\n\nNoopJS is built on a simple philosophy: the web doesn't need another framework. It needs one that gets out of the way.\n\n**LCP 0.06s · CLS 0 · INP 40ms · 0 KB static · 466 B resume · 317 B + 3.5 KB cached SPA**\n\n*v1.1.0*", "url": "https://wpnews.pro/news/show-hn-noopjs-a-compiler-that-ships-0-kb-javascript-for-static-pages", "canonical_source": "https://github.com/noop-js/noopjs", "published_at": "2026-07-21 18:57:19+00:00", "updated_at": "2026-07-21 19:13:02.713213+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["NoopJS", "React", "Vue", "Angular", "Tailwind CSS", "Vite"], "alternates": {"html": "https://wpnews.pro/news/show-hn-noopjs-a-compiler-that-ships-0-kb-javascript-for-static-pages", "markdown": "https://wpnews.pro/news/show-hn-noopjs-a-compiler-that-ships-0-kb-javascript-for-static-pages.md", "text": "https://wpnews.pro/news/show-hn-noopjs-a-compiler-that-ships-0-kb-javascript-for-static-pages.txt", "jsonld": "https://wpnews.pro/news/show-hn-noopjs-a-compiler-that-ships-0-kb-javascript-for-static-pages.jsonld"}}