
Just-in-Time JavaScript
Just-in-Time JavaScript ź“ė Ø
Static site generators are cool but they require a build step.
Frameworks like SvelteKit use Vite. For development, Vite basically does the build in the background. It caches and compiles to disk. It uses fancy tricks like hot module replacement to streamline the dev experience. Spicy. When itās time to deploy to production there is a slow build step to generate static files.
Frameworks like Fresh boast ājust-in-time renderingā. Instead of a build step, requests are built and served on the fly. Fresh uses esbuild WASM to bundle and render Preact. The ābuildā still happens, and can be cached, itās just less intrusive to deployment.
I like the idea of rendering server side Svelte templates just-in-time. Thatās basically how PHP works! Letās do it ourselves.
First requirement:
Dynamic Imports
A single Svelte component eventually compiles into a pure JavaScript function that returns an HTML string. For now forget the framework and focus on a render function:
function render() {
return '<h1>Hello, World!</h1>';
}
export default render;
I need to import this and call render().
Obviously you might think:
import render from './component.js';
render();
But eventually this code will be the output of some compilation and never written to a file. For that I need to use dynamic imports.
Hereās one way:
const code = `
function render() {
return '<h1>Hello, World!</h1>';
}
export default render;
`;
const url = `data:text/javascript;base64,${btoa(code)}`;
const mod = await import(url);
console.log(mod.default());
Iām using the native base64 global function for this example. In practice I would use something like encodeBase64 from the Deno standard library (denoland/deno_std) (itās cross-runtime). Iām not sure if base64 it technically required.
This may also work:
const url = `data:text/javascript,${code}`;
Data URI imports are supported by Deno, Node, Firefox, Chromium browsers, and Safari. The Safari dev console required me to wrap it in an async function. Only Bun errors.
Using a data URI feels a little hacky ā I donāt know, is it?
Another way is to use a Blob:
const code = `
function render() {
return '<h1>Hello, World!</h1>';
}
export default render;
`;
const blob = new Blob([code], {type: 'text/javascript'});
const url = URL.createObjectURL(blob);
const mod = await import(url);
URL.revokeObjectURL(url);
console.log(mod.default());
Now that feels like Iām JavaScripting correctly!
This works in all browsers. Bun errors again, as does Node this time (not supported yet). Iāve opened a Bun GitHub issue (oven-sh/bun). Bun has the same issue creating dynamic Workers.
Some JavaScript environments, namely Deno Deploy, do not support this. Deno Deploy only allows statically analyzable dynamic imports. What Iām doing is the complete opposite. Itās worth noting Deno Deploy is a hosting platform ā not a requirement to use the Deno runtime itself. You can host full-fat Deno anywhere.
There is another technique that works everywhere.
The wrong way:
const code = `
globalThis['render'] = function() {
return '<h1>Hello, World!</h1>';
}`;
eval(code);
console.log(render());
MDN eval documentation has a whole article on the global eval() function. An interesting read, TL;DR: never use eval. It is very bad practice.
The better way:
const code = `
'use strict';
function render() {
return '<h1>Hello, World!</h1>';
}
return {default: render};
`;
const mod = Function(code)();
console.log(mod.default());
This is a little safer than eval and makes use of the return statement to mimic a module export. I would discourage doing this client side in a web browser. Websites should set a content security policy to block this entirely.
Anyway, thatās dynamic imports. Letās take a step back to where the render function came from and why this is useful.
Just-in-time Svelte
A Svelte component file is a mix of JavaScript, HTML, and CSS (optional).
Letās use this basic example:
<script>
export let heading;
</script>
<h1>{heading}</h1>
To render this component I first need to convert it to pure JavaScript. Thankfully the Svelte compiler does the hard work for us.
(Iām using Deno for the following examples.)
import * as svelte from 'npm:svelte/compiler';
const component = `
<script>
export let heading;
</script>
<h1>{heading}</h1>
`;
let {js: {code}} = svelte.compile(component, {
generate: 'ssr'
});
console.log(code);
This will output the Svelte component code:
import { create_ssr_component, escape } from "svelte/internal";
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
let { heading } = $$props;
if ($$props.heading === void 0 && $$bindings.heading && heading !== void 0) $$bindings.heading(heading);
return `<h1>${escape(heading)}</h1>`;
});
export default Component;
The create_ssr_component function is what generates the actual render function when this code is imported and executed. I can do that by using the technique demonstrated earlier.
import * as svelte from 'npm:svelte/compiler';
const component = `<script>export let heading;</script><h1>{heading}</h1>`;
let {js: {code}} = svelte.compile(component, {generate: 'ssr'});
// Fix the dependency import path for Deno
code = code.replace('"svelte/internal"', '"npm:svelte/internal"');
const blob = new Blob([code], {type: 'text/javascript'});
const url = URL.createObjectURL(blob);
const mod = await import(url);
URL.revokeObjectURL(url);
console.log(
mod.default.render({
heading: 'Hello, World!'
}).html
);
This will output the rendered HTML:
<h1>Hello, World!</h1>
Pretty simple, right? Iām rendering Svelte on the fly without writing to disk. However, using Svelte to template an entire web page is going to take multiple components.
Letās compile this example:
<script>
import Heading from './header.svelte';
</script>
<Heading text="Hello, World!" />
The resulting JavaScript:
import { create_ssr_component, validate_component } from "svelte/internal";
import Heading from './header.svelte';
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
return `${validate_component(Heading, "Heading").$$render($$result, { text: "Hello, World!" }, {}, {})}`;
});
export default Component;
Do you see the problem? The Svelte compiler is not a bundler.
In this example it does not handle the Heading child component. Trying to import this code will error because the relative ./header.svelte import does not exist. And even if it did, itās not JavaScript yet. I would need to recusively compile and bundle all sub-components into a single file.
Bundling
As mentioned earlier, The Fresh framework uses esbuild to compile and bundle Preact. There is a Svelte esbuild plugin (EMH333/esbuild-svelte) too. Iām actually doing something similar to render my own website (dbushell/dbushell.com). Although Iām generating a static site, rather than serving requests on the fly.
In theory bundling Svelte for direct import and render isnāt too complicated. Against all common sense I decide to write my own!
Meet the š Svelte Bumble (dbushell/svelte-bumble) bundler and importer (for Deno).
This is experimental and will never be a serious contender but itās a fun little project. Bumble is extremely fragile right now. Itās not parsing any JavaScript; just using regular expressions to find & replace imports and exports.
Iāve even gone so far as to code my own just-in-time web framework. Itās like a lightweight mix of ideas from Fresh and SvelteKit. Take an early look at š¦ DinoSsr (dbushell/dinossr) if youāre curious. Right now DinoSsr only generates static sites. I will probably just use esbuild in the end so I can deliver front-end bundles for hydration.