Vanilla JS islands: complete HTML from the server, JavaScript only where you touch
· 3 min read
A classic SPA sends an empty shell and waits for JavaScript before anything is readable. That pattern made sense for application UIs that are almost entirely interactive. It is a poor default for marketing sites, docs, and blogs — where the primary job is to deliver a document.
Island architecture flips the default. The server renders the full page. Each interactive region is a small module that loads when strategy says so. The article body, navigation labels, and footer links are HTML first. JavaScript is permissioned, not assumed.
JSkelet’s islands are deliberately vanilla. There is no React runtime on content pages, no hydration of a virtual tree for the whole document, and no “client component boundary” that quietly pulls half the app into the browser.
The island contract in practice
In a .jsk template you mark a node:
<div data-island="theme-toggle"></div>
Optional props travel as JSON on data-island-props. The client entry registers a dynamic import map from island name to module via registerAll. Each module exports mount(element, props) and may return a cleanup function.
That is the whole public contract. No JSX compiler for islands. No framework scheduler. DOM APIs and helpers from jskelet/client are enough for theme toggles, mobile nav, copy buttons, and product demos.
Hydration strategy is explicit because invisible defaults become performance bugs:
- Default (visible) — attach when the island scrolls into view. Most widgets belong here.
- Eager — global chrome that must work early (theme, primary nav disclosure).
- Idle — heavy or non-critical work after
loadand idle time.
If everything is eager, you rebuilt a SPA entrypoint with extra attributes. If everything is idle, first interaction feels broken. The art is picking the minority of islands that deserve eagerness.
Progressive enhancement is not nostalgia
Forms, lists, and article bodies should work with JavaScript disabled. That is how you survive broken networks, corporate proxies, and crawlers that never execute your bundle.
Islands then add the polish: search filters, theme persistence, animated explainers, media helpers. They never own the primary content. When an island fails to load, the page remains a page.
This also simplifies QA. You can read the HTML response in curl and see the product. You do not need a headless browser to know whether the H1 shipped.
Why JSkelet does not default to React islands
React islands are a valid pattern in other ecosystems. In JSkelet they fight the product pitch. Presentational UI is already HTML from .jsk or server-side views/components/*.js helpers. Adding React for a theme toggle means shipping a second UI model, a second mental package of hooks and effects, and a larger client graph for a problem mount() solves in a few dozen lines.
If your design system is already React, you can still adopt JSkelet for public content and keep React inside a private app — or port the interactive pieces to vanilla islands over time. The framework’s bet is clear: complete HTML first for content URLs.
Performance and SEO side effects
Fewer bytes of JavaScript means less main-thread work after load. Islands that wait for visibility do not compete with LCP candidates. Complete HTML means crawlers see the copy without a rendering lottery.
None of that absolves bad content or slow origins. It does remove a whole class of self-inflicted failures: blank first paints, “content appears after hydrate,” and INP spikes from shipping an application runtime to read a blog post.
Boundaries that keep islands honest
- Do not turn the whole page into one island.
- Do not fetch the article body from the client when the server already had it.
- Do not hide primary navigation exclusively behind JS.
- Do put shared state in a tiny store only when two islands truly share it — most do not.
Ship less JavaScript, keep the page readable on day one, and still add rich interaction where it earns its weight — that is the islands rule JSkelet is built around.
Comments
No comments yet.