Skip to content

Commit bbefb51

Browse files
Merge pull request #23 from posecode-dev/perf/defer-codemirror
2 parents 851010f + 6bfedae commit bbefb51

1 file changed

Lines changed: 29 additions & 17 deletions

File tree

playground/src/main.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { parse } from "posecode-parser";
1111
import { inject } from "@vercel/analytics";
1212
import type { Viewer } from "posecode-render";
1313
import { buildNiceShareHash, resolveSharedSource } from "./nice-share.js";
14-
import { createPosecodeEditor, type PosecodeEditor } from "./editor.js";
14+
import type { PosecodeEditor } from "./editor.js";
1515
import { PRESETS } from "./presets.js";
1616
import { renderWarnings } from "./warnings.js";
1717
import llmPrompt from "../../spec/llm-authoring.md?raw";
@@ -21,7 +21,10 @@ inject();
2121
const $ = <T extends HTMLElement>(id: string): T =>
2222
document.getElementById(id) as T;
2323

24-
let editorApi: PosecodeEditor; // assigned at boot, once the initial doc is known
24+
// CodeMirror is heavy too, so the editor is lazy-loaded after first paint (its
25+
// own chunk). Until that resolves, `editorApi` is null; recompile() and the
26+
// user-action handlers guard on it (a click in that ~tens-of-ms window no-ops).
27+
let editorApi: PosecodeEditor | null = null;
2528
const warnings = $<HTMLDivElement>("warnings");
2629
const canvas = $<HTMLCanvasElement>("canvas");
2730
const playpause = $<HTMLButtonElement>("playpause");
@@ -156,7 +159,11 @@ function scheduleRecompile(): void {
156159

157160
function recompile(): void {
158161
window.clearTimeout(debounce); // cancel any pending run; we're compiling now
159-
const source = editorApi.getValue();
162+
// Captured once so control-flow narrowing survives the calls below; a null
163+
// editor (still loading) means there's nothing to compile yet.
164+
const ed = editorApi;
165+
if (!ed) return;
166+
const source = ed.getValue();
160167
if (!source.trim()) {
161168
// A deliberately blank editor (the "New" flow): a paste target, not an
162169
// error state. Show a hint instead of parse errors and stop the playback.
@@ -181,10 +188,10 @@ function recompile(): void {
181188
updateReps();
182189
buildRibbonAndMarkers();
183190
phaseRanges = computePhaseRanges(
184-
editorApi.getValue(),
191+
ed.getValue(),
185192
(tl?.segments ?? []).map((s) => s.name),
186193
);
187-
editorApi.highlightPhase(null); // next onPhase paints the active block
194+
ed.highlightPhase(null); // next onPhase paints the active block
188195
}
189196
}
190197

@@ -326,7 +333,7 @@ function loadPreset(id: string): void {
326333
if (!preset) return;
327334
currentPresetId = preset.id;
328335
libCurrent.textContent = preset.label;
329-
editorApi.setValue(preset.source);
336+
editorApi?.setValue(preset.source);
330337
recompile();
331338
setMobileView("viewer"); // on phones, jump to the figure after picking
332339
}
@@ -345,10 +352,10 @@ renderLibraryList();
345352
$<HTMLButtonElement>("new-doc").addEventListener("click", () => {
346353
currentPresetId = null;
347354
libCurrent.textContent = "New movement";
348-
editorApi.setValue("");
355+
editorApi?.setValue("");
349356
recompile(); // swaps the status row to the blank-editor hint immediately
350357
setMobileView("editor"); // the paste target, front and center on phones
351-
editorApi.focus();
358+
editorApi?.focus();
352359
});
353360

354361
// --- Mobile Editor/Viewer toggle (no-op visually on desktop, where both show) ---
@@ -402,6 +409,7 @@ copyBtn.addEventListener("click", () => copyPrompt(copyBtn));
402409
// Snapshot the current document into a URL hash, reflect it in the address bar
403410
// (so it's bookmarkable), and copy the full link to the clipboard.
404411
async function shareLink(): Promise<void> {
412+
if (!editorApi) return; // editor still loading; nothing to snapshot yet
405413
try {
406414
const hash = buildNiceShareHash(editorApi.getValue());
407415
const url = `${location.origin}${location.pathname}${hash}`;
@@ -483,17 +491,21 @@ if (sharedSource) {
483491
libCurrent.textContent = PRESETS[0]!.label;
484492
}
485493

486-
editorApi = createPosecodeEditor($("editor"), {
487-
doc: initialDoc,
488-
onChange: scheduleRecompile,
494+
// Boot the two heavyweights (CodeMirror editor + Three.js renderer) after the
495+
// shell paints, each in its own lazy chunk, so neither is on the critical path.
496+
// They load independently; recompile() self-guards until both are ready, and
497+
// whichever resolves last triggers the parse-and-animate pass.
498+
499+
// Editor: mount CodeMirror into the shell, then compile so warnings surface.
500+
void import("./editor.js").then(({ createPosecodeEditor }) => {
501+
editorApi = createPosecodeEditor($("editor"), {
502+
doc: initialDoc,
503+
onChange: scheduleRecompile,
504+
});
505+
recompile();
489506
});
490-
// Parse + surface warnings immediately so the editor is useful at first paint,
491-
// even though the 3D figure appears a beat later once the renderer chunk loads.
492-
recompile();
493507

494-
// Boot the renderer after first paint: dynamic import keeps Three.js off the
495-
// critical path (its own chunk), mirroring the landing page. Once the viewer
496-
// exists we wire its callbacks and recompile so the current doc animates.
508+
// Renderer: keep Three.js off the critical path, mirroring the landing page.
497509
void import("posecode-render").then(({ createViewer }) => {
498510
viewer = createViewer(canvas);
499511
// Exposed for capture/e2e tooling (frame capture drives README GIFs).

0 commit comments

Comments
 (0)