发布

  • perf(classroom): speed up classroom loading (media hydration, sidebar thumbnails, media range requests) (#1276)

    frostbyte_neo 发布于 2026-08-29 19:48:04 +00:00

    • perf(classroom): defer non-priority media blob hydration off the load path

    Entering a classroom awaited the full mediaFiles restore — one object URL
    per restored image/video blob — before the loading gate opened. For
    video-heavy courses that is hundreds of MB of IndexedDB materialization
    blocking first paint.

    Split the restore into two phases:

    • The awaited phase now builds metadata-complete task entries but only
      creates object URLs for failed rows (none needed) and for media
      referenced by the scene the classroom opens on (persisted cursor, else
      the first scene). buildRestoredMediaTasks gains an optional
      shouldHydrateBlob predicate, defaulting to eager hydration so existing
      callers are unchanged.
    • Remaining blob-backed records hydrate in the background, chunked over
      requestIdleCallback (setTimeout fallback). Each deferred task stays
      'done' — so generation resume never re-runs it — but carries no
      objectUrl yet, which the media resolution state machine already renders
      as a pending skeleton until the URL lands.

    Background hydration is guarded per record: a task that was replaced
    (classroom switch, regeneration, retry) or that belongs to another stage
    is skipped and its freshly minted URLs are revoked immediately.

    • perf(classroom): lazy-render slide thumbnails in the playback sidebar

    The playback scene sidebar mounted a full SlideCanvas for every slide
    scene the moment the classroom opened (and every off-screen video element
    opened a preload="metadata" fetch), because SlideThumbnail's existing
    visible prop was never passed.

    Extract the editor nav rail's near-viewport IntersectionObserver hook to
    lib/hooks/use-near-viewport.ts and gate the sidebar's slide thumbnails
    through it: only scenes within 200px of the viewport render the live
    canvas; the rest show SlideThumbnail's existing placeholder until scrolled
    near. The placeholder keeps the same box size, so gating never shifts
    layout.

    The shared hook now starts hidden instead of eager: with the previous
    eager-initial state, opening a long deck still mounted every canvas for a
    frame before the observer could flip off-screen items off. The observer's
    guaranteed initial delivery flips near-viewport items within a frame; the
    no-IntersectionObserver fallback (e.g. jsdom) defers to a microtask so
    the effect never synchronously re-renders.

    • perf(media): lazy image decoding and HTTP Range support for classroom media

    Renderer: BaseImageElement's now carries loading="lazy" and
    decoding="async", so thumbnail-heavy surfaces (playback sidebar, editor
    nav rail, course cards) no longer fetch and decode every slide image up
    front. In-viewport images are unaffected — the browser fetches them
    immediately. slideToPng forces eager loading inside its permanently
    off-screen snapshot tree, where lazy images would otherwise never fetch
    and exports would capture blank slides.

    Server: GET /api/classroom-media/[classroomId]/[...path] now answers
    single byte-range requests with 206 Partial Content (Content-Range,
    Accept-Ranges, correct Content-Length), enabling progressive playback and
    seeking for hosted video/audio instead of downloading whole files. Suffix
    ranges are supported; unsatisfiable ranges get 416 with the full size;
    unsupported units or multi-range sets fall back to the plain 200 full-body
    response, which is always a legal answer. Existing caching headers are
    kept on every response shape.

    • chore(renderer): bump version to 0.1.4 for image lazy-loading change

    • fix(classroom): guard deferred hydration against restarted tasks and bound idle waits

    A deferred record is only ever 'done' without an objectUrl; a task that
    regeneration or retry restarted passes through pending/generating, so skip
    those instead of attaching stale persisted bytes. Also give the idle
    scheduling a timeout so a busy main thread cannot starve hydration.

    • fix(classroom): stop superseded deferred hydration before minting URLs

    A restore epoch captured at apply time now gates each idle chunk: loading
    another classroom or reloading the same one invalidates any older hydration
    loop, so it neither keeps scheduling work for an abandoned classroom nor
    attaches bytes read by an older load to a newer load's tasks.

    • fix(classroom): keep 416s uncached and classify element-keyed media as priority

    A 416 with public immutable caching can poison the media URL for later
    valid requests, so range errors now send Cache-Control: no-store. Priority
    classification also collects the opening scene's media element ids, since
    task lookup binds records keyed stage: even when the slide slot
    carries a different opaque ref.

    • fix(classroom): tie deferred hydration liveness to the classroom load token

    The restore epoch only advanced when the next load reached apply, so an
    abandoned classroom kept hydrating during the next load's storage/network
    phase. Compose the epoch with the load's isCurrent (load token plus effect
    cleanup) so navigation stops the loop at the next idle boundary.

    • fix(classroom): classify legacy-recovered media before deferring it

    Legacy singleton video recovery assigns a placeholderRef only at the end of
    the task build, so a record keyed by an allocated id without placeholderRef
    was deferred even when it backs the opening scene's gen_vid_* element. Run a
    metadata-only build first to learn each record's effective ref and classify
    against it, keeping the first visible page's legacy video eager.

    • fix(action): wait for deferred video bytes before starting play_video

    executePlayVideo treated status done as immediately playable, but a deferred
    restore is done without an objectUrl: the renderer shows a skeleton, no

    • fix(classroom): include the stage whiteboard in priority media refs

    The stage-level whiteboard stays open across standalone classroom switches,
    so its media can be visible before any scene is. Classifying it as deferred
    left visible whiteboard media pending behind idle hydration chunks.

    下载附件