mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2132 from PR0M3TH3AN/perf/docs-scrollspy-intersection-observer-13192433827341130305
⚡ Optimize Docs ScrollSpy with IntersectionObserver
This commit is contained in:
4 files changed
+145
-88
No files matched your search
Vendored
+47
-26
@@ -29,6 +29,8 @@ const scrollSpyState = {
|
||||
activeId: "",
|
||||
rafId: null,
|
||||
onScroll: null,
|
||||
observer: null,
|
||||
passedHeadings: null,
|
||||
};
|
||||
|
||||
function getHashParams() {
|
||||
@@ -260,15 +262,11 @@ function resetSectionHighlight() {
|
||||
}
|
||||
|
||||
function clearScrollSpy() {
|
||||
if (scrollSpyState.onScroll && typeof window !== "undefined") {
|
||||
window.removeEventListener("scroll", scrollSpyState.onScroll);
|
||||
window.removeEventListener("resize", scrollSpyState.onScroll);
|
||||
if (scrollSpyState.observer) {
|
||||
scrollSpyState.observer.disconnect();
|
||||
scrollSpyState.observer = null;
|
||||
}
|
||||
if (scrollSpyState.rafId && typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(scrollSpyState.rafId);
|
||||
}
|
||||
scrollSpyState.rafId = null;
|
||||
scrollSpyState.onScroll = null;
|
||||
scrollSpyState.passedHeadings = null;
|
||||
scrollSpyState.headings = [];
|
||||
scrollSpyState.linkLookup = new Map();
|
||||
resetSectionHighlight();
|
||||
@@ -328,6 +326,7 @@ function setupScrollSpy(container) {
|
||||
const headings = Array.from(
|
||||
container.querySelectorAll("h2[id], h3[id], h4[id], h5[id]")
|
||||
);
|
||||
console.log(`[Spy] setup found ${headings.length} headings`);
|
||||
if (headings.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -345,34 +344,56 @@ function setupScrollSpy(container) {
|
||||
scrollSpyState.headings = trackedHeadings;
|
||||
scrollSpyState.linkLookup = linkLookup;
|
||||
|
||||
const updateActiveFromScroll = () => {
|
||||
scrollSpyState.rafId = null;
|
||||
const offset = 96;
|
||||
let nextId = scrollSpyState.headings[0]?.id || "";
|
||||
for (const heading of scrollSpyState.headings) {
|
||||
const top = heading.getBoundingClientRect().top - offset;
|
||||
if (top <= 0) {
|
||||
nextId = heading.id;
|
||||
const observerCallback = (entries) => {
|
||||
const passed = scrollSpyState.passedHeadings || new Set();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
// If the heading intersects (enters the viewport from top or bottom),
|
||||
// we check if it is above the offset.
|
||||
if (entry.isIntersecting) {
|
||||
// If it is intersecting, it is definitely "visible" in the viewport (minus top margin).
|
||||
// It means it's not "passed" (above the top).
|
||||
passed.delete(entry.target.id);
|
||||
} else {
|
||||
// If not intersecting, check if it's above.
|
||||
// rootMargin top is -96px. The root starts at 96px from top.
|
||||
// If entry.boundingClientRect.top < 96, it is above the root.
|
||||
if (entry.boundingClientRect.top < 96) {
|
||||
passed.add(entry.target.id);
|
||||
} else {
|
||||
passed.delete(entry.target.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scrollSpyState.passedHeadings = passed;
|
||||
|
||||
let nextId = "";
|
||||
// Find the last heading in the passed set (by DOM order)
|
||||
for (let i = scrollSpyState.headings.length - 1; i >= 0; i--) {
|
||||
const id = scrollSpyState.headings[i].id;
|
||||
if (passed.has(id)) {
|
||||
nextId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fallback to the first heading if none are passed yet (scrolled to top)
|
||||
if (!nextId && scrollSpyState.headings.length > 0) {
|
||||
nextId = scrollSpyState.headings[0].id;
|
||||
}
|
||||
|
||||
if (nextId) {
|
||||
setActiveSection(nextId);
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
if (scrollSpyState.rafId) {
|
||||
return;
|
||||
}
|
||||
scrollSpyState.rafId = requestAnimationFrame(updateActiveFromScroll);
|
||||
};
|
||||
const observer = new IntersectionObserver(observerCallback, {
|
||||
rootMargin: "-96px 0px 0px 0px",
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
scrollSpyState.onScroll = onScroll;
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", onScroll);
|
||||
onScroll();
|
||||
scrollSpyState.headings.forEach((heading) => observer.observe(heading));
|
||||
scrollSpyState.observer = observer;
|
||||
}
|
||||
|
||||
function resolveDocItem(slug) {
|
||||
|
||||
Vendored
+24
-16
@@ -301,6 +301,13 @@ export function createExploreDiversitySorter({
|
||||
const selected = [];
|
||||
const remaining = [...candidates];
|
||||
|
||||
// Cache max similarity for each remaining candidate against the selected set
|
||||
// Map<CandidateItem, { penalty: number, similarItemId: string }>
|
||||
const similarityCache = new Map();
|
||||
for (const candidate of remaining) {
|
||||
similarityCache.set(candidate, { penalty: 0, similarItemId: "" });
|
||||
}
|
||||
|
||||
while (remaining.length > 0) {
|
||||
let bestCandidate = null;
|
||||
let bestMmr = Number.NEGATIVE_INFINITY;
|
||||
@@ -326,22 +333,7 @@ export function createExploreDiversitySorter({
|
||||
|
||||
for (const candidate of remaining) {
|
||||
const rawScore = normalizeScore(candidate?.metadata?.exploreScore);
|
||||
let penalty = 0;
|
||||
let similarItemId = "";
|
||||
|
||||
if (selected.length > 0) {
|
||||
const candidateVector = vectors.get(candidate);
|
||||
for (const selectedItem of selected) {
|
||||
const similarity = cosineSimilarity(
|
||||
candidateVector,
|
||||
vectors.get(selectedItem),
|
||||
);
|
||||
if (similarity > penalty) {
|
||||
penalty = similarity;
|
||||
similarItemId = stableVideoId(selectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
const { penalty, similarItemId } = similarityCache.get(candidate);
|
||||
|
||||
const mmrScore =
|
||||
normalizedLambda * rawScore - (1 - normalizedLambda) * penalty;
|
||||
@@ -404,6 +396,22 @@ export function createExploreDiversitySorter({
|
||||
const index = remaining.indexOf(bestCandidate);
|
||||
if (index >= 0) {
|
||||
remaining.splice(index, 1);
|
||||
similarityCache.delete(bestCandidate);
|
||||
}
|
||||
|
||||
// Update similarity cache for remaining candidates against the newly selected item
|
||||
const bestCandidateVector = vectors.get(bestCandidate);
|
||||
for (const candidate of remaining) {
|
||||
const candidateVector = vectors.get(candidate);
|
||||
const similarity = cosineSimilarity(
|
||||
candidateVector,
|
||||
bestCandidateVector,
|
||||
);
|
||||
const cache = similarityCache.get(candidate);
|
||||
if (similarity > cache.penalty) {
|
||||
cache.penalty = similarity;
|
||||
cache.similarItemId = stableVideoId(bestCandidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+28
-20
@@ -26,7 +26,7 @@ function sanitizeProfileString(value) {
|
||||
class ProfileCache {
|
||||
constructor() {
|
||||
this.activePubkey = null;
|
||||
this.memoryCache = new Map(); // For runtime decrypted data: pubkey:section -> data
|
||||
this.memoryCache = new Map(); // For runtime decrypted data: pubkey -> Map(section -> data)
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
@@ -240,9 +240,11 @@ class ProfileCache {
|
||||
|
||||
getProfileData(pubkey, section) {
|
||||
// 1. Check memory cache (runtime data)
|
||||
const memKey = `${pubkey}:${section}`;
|
||||
if (this.memoryCache.has(memKey)) {
|
||||
return this.memoryCache.get(memKey);
|
||||
if (this.memoryCache.has(pubkey)) {
|
||||
const userCache = this.memoryCache.get(pubkey);
|
||||
if (userCache.has(section)) {
|
||||
return userCache.get(section);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load from persistence
|
||||
@@ -273,7 +275,10 @@ class ProfileCache {
|
||||
}
|
||||
|
||||
// Populate memory cache to avoid repeat parsing
|
||||
this.memoryCache.set(memKey, parsed);
|
||||
if (!this.memoryCache.has(pubkey)) {
|
||||
this.memoryCache.set(pubkey, new Map());
|
||||
}
|
||||
this.memoryCache.get(pubkey).set(section, parsed);
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -302,39 +307,42 @@ class ProfileCache {
|
||||
}
|
||||
|
||||
// Update memory cache
|
||||
const memKey = `${pubkey}:${section}`;
|
||||
this.memoryCache.set(memKey, data);
|
||||
if (!this.memoryCache.has(pubkey)) {
|
||||
this.memoryCache.set(pubkey, new Map());
|
||||
}
|
||||
this.memoryCache.get(pubkey).set(section, data);
|
||||
|
||||
this.emit("update", { pubkey, section, data });
|
||||
this.emit("partition-updated", { pubkey, key: section });
|
||||
}
|
||||
|
||||
setMemoryDataForPubkey(pubkey, section, data) {
|
||||
const memKey = `${pubkey}:${section}`;
|
||||
this.memoryCache.set(memKey, data);
|
||||
if (!this.memoryCache.has(pubkey)) {
|
||||
this.memoryCache.set(pubkey, new Map());
|
||||
}
|
||||
this.memoryCache.get(pubkey).set(section, data);
|
||||
|
||||
this.emit("update", { pubkey, section, data });
|
||||
}
|
||||
|
||||
setMemoryData(section, data) {
|
||||
if (!this.activePubkey) return;
|
||||
const key = `${this.activePubkey}:${section}`;
|
||||
this.memoryCache.set(key, data);
|
||||
if (!this.memoryCache.has(this.activePubkey)) {
|
||||
this.memoryCache.set(this.activePubkey, new Map());
|
||||
}
|
||||
this.memoryCache.get(this.activePubkey).set(section, data);
|
||||
}
|
||||
|
||||
getMemoryData(section) {
|
||||
if (!this.activePubkey) return null;
|
||||
const key = `${this.activePubkey}:${section}`;
|
||||
return this.memoryCache.get(key);
|
||||
if (this.memoryCache.has(this.activePubkey)) {
|
||||
return this.memoryCache.get(this.activePubkey).get(section);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
clearMemoryCache(pubkey) {
|
||||
// Clear all entries starting with pubkey:
|
||||
const prefix = `${pubkey}:`;
|
||||
for (const key of this.memoryCache.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
this.memoryCache.delete(pubkey);
|
||||
}
|
||||
|
||||
clearSignerRuntime(pubkey) {
|
||||
|
||||
+46
-26
@@ -29,6 +29,8 @@ const scrollSpyState = {
|
||||
activeId: "",
|
||||
rafId: null,
|
||||
onScroll: null,
|
||||
observer: null,
|
||||
passedHeadings: null,
|
||||
};
|
||||
|
||||
function getHashParams() {
|
||||
@@ -260,15 +262,11 @@ function resetSectionHighlight() {
|
||||
}
|
||||
|
||||
function clearScrollSpy() {
|
||||
if (scrollSpyState.onScroll && typeof window !== "undefined") {
|
||||
window.removeEventListener("scroll", scrollSpyState.onScroll);
|
||||
window.removeEventListener("resize", scrollSpyState.onScroll);
|
||||
if (scrollSpyState.observer) {
|
||||
scrollSpyState.observer.disconnect();
|
||||
scrollSpyState.observer = null;
|
||||
}
|
||||
if (scrollSpyState.rafId && typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(scrollSpyState.rafId);
|
||||
}
|
||||
scrollSpyState.rafId = null;
|
||||
scrollSpyState.onScroll = null;
|
||||
scrollSpyState.passedHeadings = null;
|
||||
scrollSpyState.headings = [];
|
||||
scrollSpyState.linkLookup = new Map();
|
||||
resetSectionHighlight();
|
||||
@@ -345,34 +343,56 @@ function setupScrollSpy(container) {
|
||||
scrollSpyState.headings = trackedHeadings;
|
||||
scrollSpyState.linkLookup = linkLookup;
|
||||
|
||||
const updateActiveFromScroll = () => {
|
||||
scrollSpyState.rafId = null;
|
||||
const offset = 96;
|
||||
let nextId = scrollSpyState.headings[0]?.id || "";
|
||||
for (const heading of scrollSpyState.headings) {
|
||||
const top = heading.getBoundingClientRect().top - offset;
|
||||
if (top <= 0) {
|
||||
nextId = heading.id;
|
||||
const observerCallback = (entries) => {
|
||||
const passed = scrollSpyState.passedHeadings || new Set();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
// If the heading intersects (enters the viewport from top or bottom),
|
||||
// we check if it is above the offset.
|
||||
if (entry.isIntersecting) {
|
||||
// If it is intersecting, it is definitely "visible" in the viewport (minus top margin).
|
||||
// It means it's not "passed" (above the top).
|
||||
passed.delete(entry.target.id);
|
||||
} else {
|
||||
// If not intersecting, check if it's above.
|
||||
// rootMargin top is -96px. The root starts at 96px from top.
|
||||
// If entry.boundingClientRect.top < 96, it is above the root.
|
||||
if (entry.boundingClientRect.top < 96) {
|
||||
passed.add(entry.target.id);
|
||||
} else {
|
||||
passed.delete(entry.target.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scrollSpyState.passedHeadings = passed;
|
||||
|
||||
let nextId = "";
|
||||
// Find the last heading in the passed set (by DOM order)
|
||||
for (let i = scrollSpyState.headings.length - 1; i >= 0; i--) {
|
||||
const id = scrollSpyState.headings[i].id;
|
||||
if (passed.has(id)) {
|
||||
nextId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fallback to the first heading if none are passed yet (scrolled to top)
|
||||
if (!nextId && scrollSpyState.headings.length > 0) {
|
||||
nextId = scrollSpyState.headings[0].id;
|
||||
}
|
||||
|
||||
if (nextId) {
|
||||
setActiveSection(nextId);
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
if (scrollSpyState.rafId) {
|
||||
return;
|
||||
}
|
||||
scrollSpyState.rafId = requestAnimationFrame(updateActiveFromScroll);
|
||||
};
|
||||
const observer = new IntersectionObserver(observerCallback, {
|
||||
rootMargin: "-96px 0px 0px 0px",
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
scrollSpyState.onScroll = onScroll;
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", onScroll);
|
||||
onScroll();
|
||||
scrollSpyState.headings.forEach((heading) => observer.observe(heading));
|
||||
scrollSpyState.observer = observer;
|
||||
}
|
||||
|
||||
function resolveDocItem(slug) {
|
||||
|
||||
Reference in new issue
Block a user