Load when it is needed
data-load says when an island's code is fetched: at once, when the browser is idle, or when it scrolls into view. The log says when each one came.
data-load
IntersectionObserverrequestIdleCallbackimport()
Live
When each island came
React
data-load="eager"Waiting to load ...
Preact
data-load="eager"Waiting to load ...
Vue
data-load="idle"Waiting to load ...
Solid
data-load="idle"Waiting to load ...
Svelte
data-load="visible"Far down the page - the island below loads only when it scrolls into view. Scroll on.
Svelte
data-load="visible"Not loaded yet - it is not in view.
How it is built
PAD, on the server the islands, built by Vite
<div class="cols-2">
<div>
<div class="panel-title">When each island came</div>
<div data-island="LoadLog"></div>
</div>
<div class="loads">
{loads}
<div class="card load-card">
<div class="framework-head"><strong>{$framework}</strong><code class="badge">data-load="{$load}"</code></div>
{if $load ne 'visible'}<div data-island="{$island}" data-load="{$load}" data-props="{^props}"><p class="muted small">Waiting to load ...</p></div>{/if}
</div>
{/loads}
</div>
</div>
<div class="far-below">
<p class="muted">Far down the page - the island below loads only when it scrolls into view. Scroll on.</p>
{loads}{if $load eq 'visible'}
<div class="card load-card">
<div class="framework-head"><strong>{$framework}</strong><code class="badge">data-load="{$load}"</code></div>
<div data-island="{$island}" data-load="{$load}" data-props="{^props}"><p class="muted small">Not loaded yet - it is not in view.</p></div>
</div>
{/if}{/loads}
</div><?php
// Six islands, three ways of loading: the first two at once, two when the browser is idle,
// two only when they scroll into view - far down the page. The log is an island too, loaded
// at once so it hears the others mount.
$loads = [ [ 'island' => 'ReactCounter', 'load' => 'eager', 'framework' => 'React', 'start' => 1 ],
[ 'island' => 'PreactCounter', 'load' => 'eager', 'framework' => 'Preact', 'start' => 2 ],
[ 'island' => 'VueCounter', 'load' => 'idle', 'framework' => 'Vue', 'start' => 3 ],
[ 'island' => 'SolidCounter', 'load' => 'idle', 'framework' => 'Solid', 'start' => 4 ],
[ 'island' => 'SvelteCounter', 'load' => 'visible', 'framework' => 'Svelte', 'start' => 5 ] ];
foreach ( $loads as $at => $one )
$loads [$at] ['props'] = [ 'start' => $one ['start'], 'framework' => $one ['framework'] ];
?>// pad-islands - the runtime of the islands application, for components of any framework.
//
// islands({ Counter: react(() => import('./islands/Counter.react.jsx')) })
//
// mounts a component on every element <div data-island="Counter" data-props="{^props}"> PAD
// wrote, the JSON of data-props as its props and what PAD rendered inside as serverHtml.
// A component's code - its framework with it - is fetched when its island needs it:
//
// data-load="eager" at once (the default)
// data-load="idle" once the browser has nothing else to do
// data-load="visible" when the island scrolls into view
//
// react(), vue(), svelte(), preact() and solid() pair a component with its framework's
// mount; each adapter is a module of its own, so a page without a Vue island loads no Vue.
// store() is state the islands of every framework share; get() and post() talk to PAD.
const registry = {};
function adapter(load) {
return (component) => () => Promise.all([load(), component()]).then(([a, c]) => a.island(c.default));
}
export const react = adapter(() => import('./adapters/react.js'));
export const vue = adapter(() => import('./adapters/vue.js'));
export const svelte = adapter(() => import('./adapters/svelte.js'));
export const preact = adapter(() => import('./adapters/preact.js'));
export const solid = adapter(() => import('./adapters/solid.js'));
function props(element) {
let value = {};
try { value = JSON.parse(element.getAttribute('data-props') || '{}'); } catch (e) { console.error('pad-islands: no JSON in data-props', element); }
if (!value || typeof value !== 'object' || Array.isArray(value)) value = { data: value };
value.serverHtml = element.innerHTML;
return value;
}
// Every island that mounted, told as a pad:island event and kept: a listener that came
// later - itself an island - reads what it missed with mounted().
const history = [];
function told(name, element, ms) {
const detail = { name, element, ms, at: new Date() };
history.push(detail);
window.dispatchEvent(new CustomEvent('pad:island', { detail }));
}
export const mounted = () => history.slice();
function start(element) {
if (element.padIsland) return;
const name = element.getAttribute('data-island');
const load = registry[name];
if (!load) return console.warn(`pad-islands: no island named ${name}`);
element.padIsland = 'loading';
const began = performance.now();
load().then((mount) => {
element.padIsland = mount(element, props(element));
element.classList.add('is-mounted');
told(name, element, Math.round(performance.now() - began));
});
}
function when(element) {
const how = element.getAttribute('data-load') || 'eager';
if (how === 'idle')
return (window.requestIdleCallback || ((go) => setTimeout(go, 200)))(() => start(element));
if (how === 'visible' && 'IntersectionObserver' in window) {
const seen = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) { seen.disconnect(); start(element); }
}, { rootMargin: '100px' });
return seen.observe(element);
}
start(element);
}
export function islands(list) {
Object.assign(registry, list);
document.querySelectorAll('[data-island]').forEach(when);
}
// ------------------------------------------------------------------ shared state
const stores = {};
export function store(name, initial) {
if (stores[name]) return stores[name];
let value = initial;
const listeners = new Set();
return stores[name] = {
get: () => value,
set(next) {
value = typeof next === 'function' ? next(value) : next;
listeners.forEach((listener) => listener(value));
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}
// ------------------------------------------------------------------ requests back to PAD
function address(page, params) {
let url = location.pathname + '?' + page;
for (const [key, value] of Object.entries(params || {}))
if (value !== undefined && value !== null && value !== '')
url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(value);
return url;
}
async function send(method, page, params, body) {
const init = { method, headers: { Accept: 'application/json' }, credentials: 'same-origin' };
if (body) {
init.headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]')?.content || '';
init.body = new URLSearchParams(body);
}
const response = await fetch(address(page, { ...params, padFormat: 'json' }), init);
if (!response.ok) throw Object.assign(new Error(`The server answered ${response.status}`), { status: response.status });
return response.json();
}
export const get = (page, params) => send('GET', page, params);
export const post = (page, body) => send('POST', page, {}, body);// The log of the loading page, in Preact: every island that mounted before it, and every
// one after (the pad:island event of pad-islands.js), with the time its code took to come.
import { html } from 'htm/preact';
import { useState, useEffect } from 'preact/hooks';
import { mounted } from '../pad-islands.js';
export default function LoadLog() {
const [rows, setRows] = useState(mounted);
useEffect(() => {
setRows(mounted()); // what came between render and now
const told = (event) => setRows((now) => [...now, event.detail]);
window.addEventListener('pad:island', told);
return () => window.removeEventListener('pad:island', told);
}, []);
return html`
<ol class="load-log">
${rows.length === 0 && html`<li class="muted">Waiting for the first island ...</li>`}
${rows.map((row) => html`<li class="rise"><code>${row.at.toLocaleTimeString()}</code> <strong>${row.name}</strong> mounted - its code took ${row.ms} ms <span class="badge">${row.element.getAttribute('data-load') || 'eager'}</span></li>`)}
</ol>`;
}