Skip to content

Commit 182da1e

Browse files
committed
perf: Island lazy loading v2 (fixed __lazy init + no TS in JS)
1 parent 6806ea8 commit 182da1e

3 files changed

Lines changed: 61 additions & 136 deletions

File tree

deno.lock

Lines changed: 2 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 20 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @kissjs/core - entry-generators.ts tests (Deno)
33
*
4-
* v0.5.0: Simplified client entry — no hydration strategies, no Lit imports.
4+
* v0.5.0: IntersectionObserver lazy loading.
55
*/
66
import { assertEquals, assertExists, assertFalse } from 'jsr:@std/assert@^1.0.0';
77
import { generateClientEntry } from '../src/entry-generators.ts';
@@ -18,85 +18,49 @@ const PACKAGE_ISLAND = {
1818
isPackage: true as const,
1919
};
2020

21-
// ─── Empty Islands ──────────────────────────────────────────
22-
23-
Deno.test('generateClientEntry returns no-op for empty islands', () => {
21+
Deno.test('empty islands → zero client JS', () => {
2422
const code = generateClientEntry([]);
2523
assertExists(code.includes('zero client JS needed'));
2624
});
2725

28-
// ─── Local Island Registration ─────────────────────────────
29-
30-
Deno.test('generateClientEntry registers local island via dynamic import', () => {
26+
Deno.test('local island in lazy map', () => {
3127
const code = generateClientEntry([LOCAL_ISLAND]);
3228
assertExists(code.includes("import('./islands/my-counter.ts')"));
33-
assertFalse(
34-
code.includes("import Island_0 from './islands/my-counter.ts'"),
35-
'Local islands must use dynamic import()',
36-
);
3729
});
3830

39-
// ─── Package Island Dynamic Import ─────────────────────────
40-
41-
Deno.test('generateClientEntry uses dynamic import for package islands', () => {
31+
Deno.test('package island in lazy map', () => {
4232
const code = generateClientEntry([PACKAGE_ISLAND]);
4333
assertExists(code.includes("import('@kissjs/ui/kiss-theme-toggle')"));
44-
// Should be dynamic import(), not static import
45-
assertExists(code.match(/import\s*\(/));
46-
assertFalse(
47-
code.includes(`import '${PACKAGE_ISLAND.modulePath}'`),
48-
'Package islands must use dynamic import()',
49-
);
5034
});
5135

52-
// ─── Mixed Islands ─────────────────────────────────────────
53-
54-
Deno.test('generateClientEntry handles mixed local and package islands', () => {
36+
Deno.test('mixed islands', () => {
5537
const code = generateClientEntry([LOCAL_ISLAND, PACKAGE_ISLAND]);
56-
5738
assertExists(code.includes("import('./islands/my-counter.ts')"));
5839
assertExists(code.includes("import('@kissjs/ui/kiss-theme-toggle')"));
59-
60-
assertFalse(
61-
code.includes("import Island_0 from './islands/my-counter.ts'"),
62-
'Local islands must use dynamic import()',
63-
);
6440
});
6541

66-
// ─── No Lit Hydration ──────────────────────────────────────
67-
68-
Deno.test('generateClientEntry has no Lit hydration imports', () => {
42+
Deno.test('no Lit hydration', () => {
6943
const code = generateClientEntry([LOCAL_ISLAND]);
70-
// v0.5.0: No Lit hydration — CE spec handles element upgrade
7144
assertEquals(code.includes('lit-element-hydrate-support'), false);
72-
assertEquals(code.includes('litElementHydrateSupport'), false);
7345
assertEquals(code.includes('LitElement'), false);
74-
assertEquals(code.includes('defer-hydration'), false);
75-
assertEquals(code.includes('__kissFindDeferred'), false);
76-
assertEquals(code.includes('__kissHydrateAll'), false);
7746
});
7847

79-
// ─── whenDefined List ──────────────────────────────────────
80-
81-
Deno.test('generateClientEntry creates whenDefined list for all islands', () => {
82-
const code = generateClientEntry([
83-
LOCAL_ISLAND,
84-
{ ...PACKAGE_ISLAND },
85-
{ tagName: 'code-block', modulePath: './islands/code-block.ts', isPackage: false },
86-
]);
87-
88-
assertExists(code.includes("customElements.whenDefined('my-counter')"));
89-
assertExists(code.includes("customElements.whenDefined('kiss-theme-toggle')"));
90-
assertExists(code.includes("'code-block'"));
48+
Deno.test('IntersectionObserver + lazy map', () => {
49+
const code = generateClientEntry([LOCAL_ISLAND, PACKAGE_ISLAND]);
50+
assertExists(code.includes('IntersectionObserver'));
51+
assertExists(code.includes("'my-counter': () => import('./islands/my-counter.ts')"));
52+
assertExists(code.includes("'kiss-theme-toggle': () => import('@kissjs/ui/kiss-theme-toggle')"));
9153
});
9254

93-
// ─── kiss:ready Event ──────────────────────────────────────
94-
95-
Deno.test('generateClientEntry dispatches kiss:ready event', () => {
55+
Deno.test('kiss:ready event', () => {
9656
const code = generateClientEntry([LOCAL_ISLAND]);
97-
98-
// v0.5.0: dispatches kiss:ready instead of hydration ceremony
9957
assertExists(code.includes('kiss:ready'));
100-
assertExists(code.includes('customElements.whenDefined'));
101-
assertExists(code.includes('Promise.all'));
58+
assertExists(code.includes('IntersectionObserver'));
59+
60+
// Validate JS syntax — generated code must be clean JavaScript
61+
try {
62+
new Function(code);
63+
} catch (e) {
64+
assertEquals(true, false, `Generated JS has syntax error: ${String(e)}`);
65+
}
10266
});

packages/kiss-core/src/entry-generators.ts

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,87 +3,64 @@
33
*
44
* Pure functions that generate client entry code strings.
55
*
6-
* v0.5.0: Simplified client entry.
7-
* No Lit hydration, no defer-hydration, no <!--lit-part--> markers.
8-
* Custom Elements upgrade naturally via the browser's CE spec.
9-
*
10-
* Client lifecycle:
11-
* 1. Browser parses SSR HTML → DSD Shadow DOM attached automatically
12-
* 2. This script loads → dynamic import() all island modules
13-
* 3. Each module's side-effect calls customElements.define()
14-
* 4. Browser automatically upgrades all existing <tag> elements
15-
* 5. connectedCallback fires on each upgraded element
16-
* 6. Event listeners are attached (no re-rendering needed)
17-
*
18-
* No hydration ceremony needed because:
19-
* - DSD provides the initial Shadow DOM content (from SSR)
20-
* - Custom Elements upgrade activates the behavior
21-
* - No TemplateResult to re-associate
22-
* - No <!--lit-part--> markers to reconcile
6+
* v0.5.0: Lazy Island Loading via IntersectionObserver.
7+
* Islands load only when they scroll into the viewport.
238
*/
249

25-
/** Island entry for client bundle generation */
2610
export interface ClientIslandEntry {
27-
/** Custom element tag name */
2811
tagName: string;
29-
/** Module path for dynamic import */
3012
modulePath: string;
31-
/** True if this island comes from a package */
3213
isPackage?: boolean;
3314
}
3415

35-
/**
36-
* Generate the client entry point file content.
37-
*
38-
* This entry is built by Vite's client build (Phase 2).
39-
* It dynamically imports all island modules — the browser's Custom
40-
* Elements v1 spec handles automatic element upgrade from there.
41-
*
42-
* @param islands - List of islands to register
43-
* @param strategy - Hydration strategy (preserved for backward compat, minimal effect)
44-
*/
4516
export function generateClientEntry(
4617
islands: ClientIslandEntry[],
4718
): string {
4819
if (islands.length === 0) {
4920
return '// KISS Client Entry — No islands detected, zero client JS needed\n';
5021
}
5122

52-
// All islands use dynamic import() — their side effects call customElements.define()
53-
const dynamicImports = islands
54-
.map((island) => ` import('${island.modulePath}'),`)
55-
.join('\n');
23+
const islandMap = islands
24+
.map((i) => ` '${i.tagName}': () => import('${i.modulePath}')`)
25+
.join(',\n');
5626

57-
const whenDefinedList = islands
58-
.map((island) => `customElements.whenDefined('${island.tagName}')`)
59-
.join(', ');
27+
const tags = islands.map((i) => `'${i.tagName}'`).join(', ');
6028

61-
return `// KISS Client Entry (auto-generated — v0.5.0)
62-
// No Lit hydration — CE upgrade + DSD handles everything natively.
29+
// Note: This is generated JavaScript — NO TypeScript syntax allowed.
30+
return `// KISS Client Entry (auto-generated)
31+
// IntersectionObserver lazy loading — islands load on scroll.
6332
64-
// --- Dynamic import all islands ---
65-
// Each module's customElements.define() registers the element.
66-
// The browser automatically upgrades existing SSR-rendered elements.
67-
const __islandPromises = Promise.all([
68-
${dynamicImports}
69-
]);
33+
var __lazy = {
34+
${islandMap}
35+
};
7036
71-
// --- Wait for all elements to upgrade, then notify ---
72-
__islandPromises.then(() => {
73-
Promise.all([${whenDefinedList}]).then(() => {
74-
// Dispatch custom event so external code can hook into ready state
75-
document.dispatchEvent(new CustomEvent('kiss:ready', {
76-
detail: { islands: [${islands.map((i) => `'${i.tagName}'`).join(', ')}] }
77-
}));
37+
function __upgrade(tag) {
38+
if (__lazy[tag]) {
39+
__lazy[tag]().catch(function(e) { console.warn('[KISS]', tag, e); });
40+
__lazy[tag] = null;
41+
}
42+
}
43+
44+
var __obs = new IntersectionObserver(function(entries) {
45+
entries.forEach(function(e) {
46+
if (e.isIntersecting) {
47+
__upgrade(e.target.tagName.toLowerCase());
48+
__obs.unobserve(e.target);
49+
}
7850
});
79-
}).catch(err => {
80-
console.warn('[KISS] Island loading failed:', err);
81-
// Best-effort: try to upgrade any islands that DID load
82-
Promise.all([${whenDefinedList}]).then(() => {
83-
document.dispatchEvent(new CustomEvent('kiss:ready', {
84-
detail: { islands: [${islands.map((i) => `'${i.tagName}'`).join(', ')}], partial: true }
85-
}));
86-
}).catch(() => { /* best effort */ });
8751
});
52+
53+
// Observe islands in document and kiss-layout shadow DOM
54+
var __layout = document.querySelector('kiss-layout');
55+
var __root = __layout && __layout.shadowRoot || document;
56+
var __tags = [${tags}];
57+
__tags.forEach(function(tag) {
58+
__root.querySelectorAll(tag).forEach(function(el) { __obs.observe(el); });
59+
});
60+
setTimeout(function() {
61+
document.dispatchEvent(new CustomEvent('kiss:ready', {
62+
detail: { islands: __tags }
63+
}));
64+
}, 100);
8865
`;
89-
}
66+
}

0 commit comments

Comments
 (0)