Angelitedocs ENES

Home › Go to production

SSR and precompiling

Server-side rendering with hydrate() and template compilation at build time to ship only the runtime.

Server-side rendering#

@duvanjamid/angelite/tools/ssr.mjs renders a component in Node (with jsdom, which is already a build dependency): there is no second implementation to maintain, the output is what the browser would build, with the same URL sanitizing and the same escaping. Requests are queued, so two renders at once do not step on each other.

// server (Node)
import { renderPage } from '@duvanjamid/angelite/tools/ssr.mjs';
import { provideRouter } from '@duvanjamid/angelite';

const html = await renderPage(App, {
  shell: '<!doctype html><body><!--app--><!--state--><script type="module" src="/client.js"></script></body>',
  url: req.url,                                              // path and query
  setup: () => provideRouter(routes, { mode: 'history' }),    // runs with the DOM already available
  ready: async app => { await app.users.settled; },           // wait for your data
  state: { users }                                           // reaches the client as window.__STATE__
});
// client
import { hydrate } from '@duvanjamid/angelite/hydrate';
hydrate(App, document.getElementById('app'));
  • renderToString(Comp, options) returns only the component's HTML; renderPage fills a shell (<!--app--> and <!--state-->); serializeState(obj) gives JSON that is safe to embed in a <script> (it escapes <, >, & and the line separators).
  • setup runs before mounting, with the DOM globals set: that is where you configure the router so it reads url.
  • ready receives the instance and can return a promise: that is where you wait for your data.
Hydration: what it does and what it does not

hydrate() replaces the server HTML with the live app in the same task, so there is no flicker. It does not adopt the existing nodes: the client renders again. You gain visible content before the JavaScript arrives, and SEO; you do not save the work of a full hydration. There is no streaming either.

Precompiling templates#

By default AngeLite compiles templates in the browser: one script, no build. If every KB matters to you, turn them into JavaScript during the build and ship only the runtime (no expression parser or compiler): about 6 KB gzip for a real app, a little less than Preact (6.2 KB).

<!-- counter.ng.html -->
<button (click)="inc()">Clicks: &#123;&#123; count() &#125;&#125;</button>
// counter.js
import { signal, component, mount } from '@duvanjamid/angelite/runtime';
import aot from './counter.ng.html';              // compiled by the plugin

class Counter { count = signal(0); inc() { this.count.update(n => n + 1); } }
component(Counter, { aot });                       // instead of template
mount(Counter, document.body);
// build with esbuild
import { aotPlugin } from '@duvanjamid/angelite/tools/aot.mjs';
esbuild.build({ entryPoints: ['counter.js'], bundle: true, minify: true,
  plugins: [aotPlugin({ components: ['ui-button'], members: 'auto', check: 'error' })] });

// or with Vite (reloads when you edit a template)
import angelite from '@duvanjamid/angelite/tools/vite.mjs';
export default { plugins: [angelite({ components: ['ui-button'], members: 'auto' })] };

There is also a CLI: node tools/aot.mjs template.html output.js --components ui-button,ui-card. A precompiled template produces exactly the same DOM as the one compiled in the browser (the tests check it).

The compiler also checks your templates#

  • Unknown elements (<ui-buton>), pipes that do not exist and, with members, names the component does not have ({{ counter() }} when only count exists).
  • members: 'auto' reads the fields and methods of the class next to the template (x.ng.html + x.js); you can also give the list.
  • check: 'warn' (default) warns, 'error' stops the build, false turns it off. It does not check types.
Optional modules in a precompiled app

With the runtime alone, import @duvanjamid/angelite/pipes for pipes, or use @duvanjamid/angelite/runtime-plus, which brings portals, @defer and references without the compiler. enableDevWarnings() tells you if a template needs one that is missing.

Never compile templates with user data

With in-browser compilation, a template is code. If a template is built with text you do not control, you are running that text. Precompiling removes the problem by construction: the browser only receives the result.