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;renderPagefills a shell (<!--app-->and<!--state-->);serializeState(obj)gives JSON that is safe to embed in a<script>(it escapes<,>,&and the line separators).setupruns before mounting, with the DOM globals set: that is where you configure the router so it readsurl.readyreceives the instance and can return a promise: that is where you wait for your data.
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: {{ count() }}</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, withmembers, names the component does not have ({{ counter() }}when onlycountexists). 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,falseturns it off. It does not check types.
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.
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.