mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
perf: optimize critical rendering and data processing paths
- Refactor `AmbientBackground` to use `ResizeObserver` instead of polling layout in animation loop. - Implement async chunking for `ExploreDataService` to prevent main thread blocking during heavy calculations. - Throttle `UrlHealthController` video probes to a maximum of 3 concurrent requests to prevent resource exhaustion. - Document optimization tasks in `OPTIMIZATION_TASKS.md`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
36e96306f5
commit
a0f893ace6
4 files changed
+179
-68
No files matched your search
@@ -0,0 +1,50 @@
|
||||
# Optimization Task Stubs
|
||||
|
||||
The following tasks were identified and implemented to resolve high CPU/GPU usage and potential crashes on the website.
|
||||
|
||||
## Completed Tasks
|
||||
|
||||
### 1. Optimize Ambient Background Animation
|
||||
**Problem:** The `AmbientBackground` controller was calling `resizeCanvas` inside the `requestAnimationFrame` loop. This function accessed `getBoundingClientRect()`, causing synchronous layout thrashing (forced reflow) on every frame, leading to high CPU/GPU usage.
|
||||
**Fix:**
|
||||
- Refactor `js/ui/ambientBackground.js` to use `ResizeObserver`.
|
||||
- Only update canvas dimensions when the element actually resizes.
|
||||
- Remove layout reads from the animation loop.
|
||||
|
||||
### 2. Async Chunking for Explore Data Service
|
||||
**Problem:** `ExploreDataService` performed heavy synchronous iterations over large datasets (Watch History and Video Tags) on the main thread. This could cause the UI to freeze (long tasks) during initialization or updates.
|
||||
**Fix:**
|
||||
- Refactor `buildWatchHistoryTagCounts` and `buildTagIdf` in `js/services/exploreDataService.js`.
|
||||
- Implement processing in chunks (e.g., 500 items).
|
||||
- Yield to the main thread using `setTimeout` between chunks to allow user interaction and frame rendering.
|
||||
|
||||
### 3. Throttle URL Health Video Probes
|
||||
**Problem:** The `UrlHealthController` spawned a `<video>` element for every video card to probe connectivity. When loading a grid of 50+ videos, this caused a storm of simultaneous media decoding requests, leading to resource exhaustion and browser instability.
|
||||
**Fix:**
|
||||
- Refactor `js/ui/urlHealthController.js`.
|
||||
- Implement a concurrency limiter (queue) for `probeUrlWithVideoElement`.
|
||||
- Limit active video probes to a maximum of 3 concurrent instances.
|
||||
|
||||
## Future Optimization Tasks
|
||||
|
||||
### 4. Virtualize Video List Rendering
|
||||
**Description:** Implement virtual scrolling (windowing) for the main video grid.
|
||||
**Benefit:** Reduces DOM node count and memory usage when displaying thousands of videos.
|
||||
**Action:**
|
||||
- Create a `VirtualScroller` component.
|
||||
- Only render video cards that are currently in the viewport (plus a buffer).
|
||||
|
||||
### 5. Move Heavy Computation to Web Worker
|
||||
**Description:** Move the tag IDF and Watch History analysis entirely off the main thread.
|
||||
**Benefit:** Zero impact on UI responsiveness during data refreshes.
|
||||
**Action:**
|
||||
- Create `js/workers/exploreData.worker.js`.
|
||||
- Move `ExploreDataService` logic into the worker.
|
||||
- Use `postMessage` to send video data and receive calculated stats.
|
||||
|
||||
### 6. Optimize Image Loading with `loading="lazy"` & `decoding="async"`
|
||||
**Description:** Ensure all thumbnails and avatars use modern browser lazy loading.
|
||||
**Benefit:** Faster initial page load and lower bandwidth usage.
|
||||
**Action:**
|
||||
- Audit `VideoCard` and `VideoModal` templates.
|
||||
- Ensure `<img>` tags have `loading="lazy"` and `decoding="async"`.
|
||||
@@ -6,6 +6,9 @@ import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
||||
const DEFAULT_IDF_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_HISTORY_REFRESH_INTERVAL_MS = 60 * 1000;
|
||||
const DEFAULT_REFRESH_DEBOUNCE_MS = 200;
|
||||
const YIELD_CHUNK_SIZE = 500;
|
||||
|
||||
const yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
function normalizeAddressKey(value) {
|
||||
if (typeof value !== "string") {
|
||||
@@ -99,7 +102,8 @@ export async function buildWatchHistoryTagCounts({
|
||||
return counts;
|
||||
}
|
||||
|
||||
for (const entry of items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = items[i];
|
||||
const pointer = normalizePointerInput(entry?.pointer || entry);
|
||||
let video = entry?.video || entry?.metadata?.video || null;
|
||||
|
||||
@@ -123,12 +127,16 @@ export async function buildWatchHistoryTagCounts({
|
||||
for (const tag of tags) {
|
||||
counts.set(tag, (counts.get(tag) || 0) + 1);
|
||||
}
|
||||
|
||||
if (i > 0 && i % YIELD_CHUNK_SIZE === 0) {
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function buildTagIdf({ videos } = {}) {
|
||||
export async function buildTagIdf({ videos } = {}) {
|
||||
const idf = new Map();
|
||||
const list = Array.isArray(videos) ? videos : [];
|
||||
if (!list.length) {
|
||||
@@ -138,23 +146,31 @@ export function buildTagIdf({ videos } = {}) {
|
||||
const docFrequency = new Map();
|
||||
const totalDocs = list.length;
|
||||
|
||||
for (const video of list) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const video = list[i];
|
||||
const tags = collectVideoTags(video);
|
||||
if (!tags.size) {
|
||||
continue;
|
||||
if (tags.size) {
|
||||
for (const tag of tags) {
|
||||
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
||||
if (i > 0 && i % YIELD_CHUNK_SIZE === 0) {
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
|
||||
let processedCount = 0;
|
||||
for (const [tag, df] of docFrequency.entries()) {
|
||||
const ratio = (totalDocs + 1) / (df + 1);
|
||||
const value = 1 + Math.log(ratio);
|
||||
if (Number.isFinite(value) && value > 0) {
|
||||
idf.set(tag, value);
|
||||
}
|
||||
processedCount++;
|
||||
if (processedCount % YIELD_CHUNK_SIZE === 0) {
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
|
||||
return idf;
|
||||
@@ -310,7 +326,7 @@ export default class ExploreDataService {
|
||||
}
|
||||
}
|
||||
|
||||
refreshTagIdf({ force = false, videos, reason } = {}) {
|
||||
async refreshTagIdf({ force = false, videos, reason } = {}) {
|
||||
const now = Date.now();
|
||||
if (!force && this.tagIdfUpdatedAt) {
|
||||
const elapsed = now - this.tagIdfUpdatedAt;
|
||||
@@ -326,7 +342,7 @@ export default class ExploreDataService {
|
||||
: [];
|
||||
|
||||
try {
|
||||
this.tagIdf = buildTagIdf({ videos: sourceVideos });
|
||||
this.tagIdf = await buildTagIdf({ videos: sourceVideos });
|
||||
this.tagIdfUpdatedAt = now;
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
|
||||
@@ -135,7 +135,7 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
let destroyed = false;
|
||||
let lastTime = 0;
|
||||
let tainted = false;
|
||||
let resizeBound = false;
|
||||
let resizeObserver = null;
|
||||
|
||||
const clearCanvas = () => {
|
||||
if (!canvasElement) {
|
||||
@@ -152,9 +152,12 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
cancelFrame(rafId);
|
||||
rafId = 0;
|
||||
}
|
||||
if (resizeBound && win) {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
if (win) {
|
||||
win.removeEventListener("resize", handleResize);
|
||||
resizeBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,7 +179,11 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
lastTime = timestamp;
|
||||
|
||||
try {
|
||||
const { width, height } = resizeCanvas(canvasElement, videoElement);
|
||||
// Optimization: Rely on ResizeObserver for dimensions.
|
||||
// Do NOT call resizeCanvas() here to avoid layout thrashing.
|
||||
const width = canvasElement.width || 0;
|
||||
const height = canvasElement.height || 0;
|
||||
|
||||
if (!width || !height) {
|
||||
schedule();
|
||||
return;
|
||||
@@ -203,10 +210,26 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
running = true;
|
||||
lastTime = 0;
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
if (win && !resizeBound) {
|
||||
|
||||
if (!resizeObserver && win && typeof win.ResizeObserver === "function") {
|
||||
try {
|
||||
resizeObserver = new win.ResizeObserver(() => {
|
||||
if (!destroyed) {
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
}
|
||||
});
|
||||
const host = canvasElement.parentElement || canvasElement;
|
||||
resizeObserver.observe(host);
|
||||
} catch (err) {
|
||||
if (logger && typeof logger.warn === "function") {
|
||||
logger.warn("[ambientBackground] Failed to initialize ResizeObserver", err);
|
||||
}
|
||||
}
|
||||
} else if (win && !resizeObserver) {
|
||||
// Fallback for browsers without ResizeObserver
|
||||
win.addEventListener("resize", handleResize);
|
||||
resizeBound = true;
|
||||
}
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ export default class UrlHealthController {
|
||||
this.logger = logger;
|
||||
this.constants = constants;
|
||||
this.callbacks = callbacks;
|
||||
|
||||
this.activeVideoProbes = 0;
|
||||
this.videoProbeQueue = [];
|
||||
this.MAX_CONCURRENT_VIDEO_PROBES = 3;
|
||||
}
|
||||
|
||||
getUrlHealthPlaceholderMarkup(options = {}) {
|
||||
@@ -254,67 +258,85 @@ export default class UrlHealthController {
|
||||
return { outcome: "error" };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
let settled = false;
|
||||
let timeoutId = null;
|
||||
if (this.activeVideoProbes >= this.MAX_CONCURRENT_VIDEO_PROBES) {
|
||||
await new Promise((resolve) => {
|
||||
this.videoProbeQueue.push(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
this.activeVideoProbes++;
|
||||
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
let settled = false;
|
||||
let timeoutId = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
video.removeEventListener("loadeddata", handleSuccess);
|
||||
video.removeEventListener("canplay", handleSuccess);
|
||||
video.removeEventListener("error", handleError);
|
||||
try {
|
||||
video.pause();
|
||||
} catch (err) {
|
||||
// ignore pause failures
|
||||
}
|
||||
try {
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
} catch (err) {
|
||||
// ignore cleanup failures
|
||||
}
|
||||
};
|
||||
|
||||
const settle = (result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
settle({ outcome: "ok" });
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
settle({ outcome: "error" });
|
||||
};
|
||||
|
||||
if (Number.isFinite(effectiveTimeout) && effectiveTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
settle({ outcome: "timeout" });
|
||||
}, effectiveTimeout);
|
||||
}
|
||||
video.removeEventListener("loadeddata", handleSuccess);
|
||||
video.removeEventListener("canplay", handleSuccess);
|
||||
video.removeEventListener("error", handleError);
|
||||
|
||||
try {
|
||||
video.pause();
|
||||
} catch (err) {
|
||||
// ignore pause failures
|
||||
}
|
||||
try {
|
||||
video.removeAttribute("src");
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.addEventListener("loadeddata", handleSuccess, { once: true });
|
||||
video.addEventListener("canplay", handleSuccess, { once: true });
|
||||
video.addEventListener("error", handleError, { once: true });
|
||||
video.src = trimmed;
|
||||
video.load();
|
||||
} catch (err) {
|
||||
// ignore cleanup failures
|
||||
settle({ outcome: "error", error: err });
|
||||
}
|
||||
};
|
||||
|
||||
const settle = (result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
});
|
||||
} finally {
|
||||
this.activeVideoProbes--;
|
||||
if (this.videoProbeQueue.length > 0) {
|
||||
const next = this.videoProbeQueue.shift();
|
||||
if (typeof next === "function") {
|
||||
next();
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
settle({ outcome: "ok" });
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
settle({ outcome: "error" });
|
||||
};
|
||||
|
||||
if (Number.isFinite(effectiveTimeout) && effectiveTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
settle({ outcome: "timeout" });
|
||||
}, effectiveTimeout);
|
||||
}
|
||||
|
||||
try {
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.addEventListener("loadeddata", handleSuccess, { once: true });
|
||||
video.addEventListener("canplay", handleSuccess, { once: true });
|
||||
video.addEventListener("error", handleError, { once: true });
|
||||
video.src = trimmed;
|
||||
video.load();
|
||||
} catch (err) {
|
||||
settle({ outcome: "error", error: err });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async probeUrl(url, options = {}) {
|
||||
|
||||
Reference in new issue
Block a user