Skip to content

Commit 42d17a8

Browse files
Sbussisoclaude
andcommitted
perf(hls): lazy-load hls.js so the chunk loads only when a tile mounts
hls.js is ~509 KB / ~157 KB gzip. Before this change ``HlsPlayer.jsx`` imported it statically at module top, so any route whose chunk pulled in HlsPlayer (DashboardPage via CameraCard, plus TestHlsPage) paid the full gzip cost as part of route resolution — even if the user had zero cameras and never rendered a tile. Move the import inside ``setupHls()`` (the existing async function in the player's main useEffect). The module-level dependency is gone, so the hls.js chunk no longer rides along with /dashboard's first paint. It now downloads on the first ``HlsPlayer`` mount; the browser caches it for the rest of the session. Race handling: the useEffect cleanup flips a local ``cancelled`` flag that the post-await branch checks before instantiating Hls. Without this, navigating away from /dashboard while the chunk is still in flight would leak an orphaned Hls instance. Build numbers (1080p, prod build, gzip): Before: /dashboard first paint = ~726 KB JS (index 62 + dashboard 6 + player 2 + hls 157 ≈ 727) After: /dashboard first paint = ~221 KB JS (index 62 + dashboard 6 + player 2 + preload-helper 1) hls 157 KB loads async on first tile Phase 2 item #3 from the tech-debt audit. No behaviour change for end users beyond the perf win — same player code, same player events, same Clerk-JWT xhrSetup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 48e6183 commit 42d17a8

1 file changed

Lines changed: 25 additions & 1 deletion

File tree

frontend/src/components/HlsPlayer.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { useEffect, useRef, useState } from "react"
2-
import Hls from "hls.js"
32
import { useSharedToken } from "../hooks/useSharedToken.jsx"
43

4+
// hls.js is ~500 KB / ~155 KB gzipped. Static import would bloat the main
5+
// bundle on every route — including landing/pricing/security/docs which
6+
// never play video. Dynamic import below keeps it out until a HlsPlayer
7+
// actually mounts. The first video tile takes one extra round-trip to
8+
// fetch the chunk (cached thereafter); every other route gets a faster
9+
// first paint.
10+
511
// Set to true to connect directly to CloudNode on localhost:8080
612
// Set to false to use backend proxy with authentication
713
const LOCAL_TEST_MODE = import.meta.env.VITE_LOCAL_HLS === "true"
@@ -34,8 +40,23 @@ function HlsPlayer({ cameraId, cameraName }) {
3440
const API_URL = import.meta.env.VITE_API_URL || ""
3541
const ownOrigin = API_URL || window.location.origin
3642

43+
// Cancellation flag for the async chunk load below — if the
44+
// component unmounts (or cameraId changes) while the dynamic
45+
// import is still in flight, we must NOT proceed to instantiate
46+
// an Hls that nobody will clean up. The cleanup function below
47+
// flips this to true.
48+
let cancelled = false
49+
3750
const setupHls = async () => {
3851
try {
52+
// Lazy-load hls.js. Vite splits it into its own chunk
53+
// (see comment at top of file). After the first call in
54+
// a session the browser cache serves the chunk
55+
// instantly; the cost is one round-trip on the first
56+
// video the user opens.
57+
const { default: Hls } = await import("hls.js")
58+
if (cancelled) return
59+
3960
const playlistUrl = LOCAL_TEST_MODE
4061
? `http://localhost:8080/hls/${cameraId}/stream.m3u8`
4162
: `${API_URL}/api/cameras/${cameraId}/stream.m3u8`
@@ -189,6 +210,9 @@ function HlsPlayer({ cameraId, cameraName }) {
189210
setupHls()
190211

191212
return () => {
213+
// Block the late branch in setupHls (post-await) from running
214+
// its setup if the dynamic import hasn't resolved yet.
215+
cancelled = true
192216
if (stallRef.current) {
193217
clearInterval(stallRef.current)
194218
stallRef.current = null

0 commit comments

Comments
 (0)