fix(reader): optimize rendering performance, prevent browser freeze on large technical books
- extract_outline_entries: filter PDF bookmarks by depth and major structural headings, avoiding 550 micro-snippets - renderContinuousBook: load current chapter and seamlessly append next chapters on scroll (virtual continuous) instead of dumping all chapters into DOM synchronously - renderInteractiveSpans: use native HTML template fragment instead of new DOMParser per paragraph - event delegation: handle word clicks on readerContentBody instead of adding hundreds of thousands of individual DOM event listeners - consolidate existing CCNA book in DB into 68 clean chapters
This commit is contained in:
1 parent
46d6973365
commit
c4a6edee37
3 files changed
+70
-38
No files matched your search
+28
-29
@@ -2515,7 +2515,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!currentBook || !currentBook.chapters) return;
|
||||
|
||||
currentChapterIndex = Math.max(0, Math.min(targetChapter, currentBook.chapters.length - 1));
|
||||
readerChapterSelect.value = currentChapterIndex;
|
||||
if (readerChapterSelect) readerChapterSelect.value = currentChapterIndex;
|
||||
|
||||
readerPrevChBtn.disabled = currentChapterIndex <= 0;
|
||||
readerNextChBtn.disabled = currentChapterIndex >= currentBook.chapters.length - 1;
|
||||
@@ -2528,15 +2528,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (c.term) knownWordsMap.set(c.term.trim().toLowerCase(), c);
|
||||
});
|
||||
|
||||
currentBook.chapters.forEach((_, idx) => {
|
||||
loadedChaptersSet.add(idx);
|
||||
const chFrag = buildChapterDOM(idx, knownWordsMap, idx > 0);
|
||||
readerContentBody.appendChild(chFrag);
|
||||
});
|
||||
loadedChaptersSet.add(currentChapterIndex);
|
||||
const chFrag = buildChapterDOM(currentChapterIndex, knownWordsMap, false);
|
||||
readerContentBody.appendChild(chFrag);
|
||||
|
||||
const finalFooter = buildChapterFooterDOM(currentBook.chapters.length - 1);
|
||||
if (finalFooter) {
|
||||
readerContentBody.appendChild(finalFooter);
|
||||
const footer = buildChapterFooterDOM(currentChapterIndex);
|
||||
if (footer) {
|
||||
readerContentBody.appendChild(footer);
|
||||
}
|
||||
|
||||
applyScroll(restoreAnchorIdx, restoreScrollTop, restoreFraction, currentChapterIndex);
|
||||
@@ -2871,9 +2869,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
processed = processed.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
|
||||
processed = processed.replace(/(?<!_)_([^_]+)_(?!_)/g, '<em>$1</em>');
|
||||
|
||||
// 3. Parse into DOM tree
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(`<body>${processed}</body>`, 'text/html');
|
||||
// 3. Parse into DOM tree via native template fragment
|
||||
const tpl = document.createElement('template');
|
||||
tpl.innerHTML = processed;
|
||||
|
||||
// 4. Recursive transformer: keeps formatting elements, makes text nodes interactive words
|
||||
function transformDOMNode(node) {
|
||||
@@ -2888,6 +2886,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'reader-word';
|
||||
span.dataset.word = token;
|
||||
span.dataset.context = contextText || raw;
|
||||
span.textContent = token;
|
||||
|
||||
const lower = token.toLowerCase();
|
||||
@@ -2899,11 +2898,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
span.classList.add('highlight-enabled');
|
||||
}
|
||||
|
||||
span.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
handleWordClick(token, contextText || raw, span);
|
||||
});
|
||||
|
||||
frag.appendChild(span);
|
||||
} else {
|
||||
frag.appendChild(document.createTextNode(token));
|
||||
@@ -2993,7 +2987,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return document.createTextNode('');
|
||||
}
|
||||
|
||||
Array.from(doc.body.childNodes).forEach(childNode => {
|
||||
Array.from(tpl.content.childNodes).forEach(childNode => {
|
||||
container.appendChild(transformDOMNode(childNode));
|
||||
});
|
||||
}
|
||||
@@ -3453,13 +3447,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (activeCh !== null && activeCh !== undefined && activeCh !== currentChapterIndex) {
|
||||
currentChapterIndex = activeCh;
|
||||
if (readerChapterSelect) readerChapterSelect.value = currentChapterIndex;
|
||||
if (readerChapterSelect && readerChapterSelect.options) {
|
||||
for (let i = 0; i < readerChapterSelect.options.length; i++) {
|
||||
const rawTitle = currentBook.chapters[i]?.title || `Глава ${i + 1}`;
|
||||
const icon = i < currentChapterIndex ? '✔ ' : (i === currentChapterIndex ? '▶ ' : '○ ');
|
||||
readerChapterSelect.options[i].textContent = `${icon}${rawTitle}`;
|
||||
}
|
||||
}
|
||||
if (readerPrevChBtn) readerPrevChBtn.disabled = currentChapterIndex <= 0;
|
||||
if (readerNextChBtn) readerNextChBtn.disabled = currentChapterIndex >= currentBook.chapters.length - 1;
|
||||
|
||||
@@ -3470,16 +3457,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Infinite scroll: auto-append next chapter in chapters mode
|
||||
if (readerSettings.flowMode === 'chapters' && readerSettings.autoLoadNext && !isAppendingChapter) {
|
||||
// 2. Infinite scroll: auto-append next chapter in continuous mode or chapters mode with autoload
|
||||
if ((readerSettings.flowMode === 'continuous' || (readerSettings.flowMode === 'chapters' && readerSettings.autoLoadNext)) && !isAppendingChapter) {
|
||||
const distanceToBottom = readerScrollArea.scrollHeight - (readerScrollArea.scrollTop + readerScrollArea.clientHeight);
|
||||
if (distanceToBottom < 450) {
|
||||
if (distanceToBottom < 700) {
|
||||
const loadedArr = Array.from(loadedChaptersSet);
|
||||
const highestCh = loadedArr.length > 0 ? Math.max(...loadedArr) : currentChapterIndex;
|
||||
if (currentBook && currentBook.chapters && highestCh < currentBook.chapters.length - 1) {
|
||||
isAppendingChapter = true;
|
||||
appendNextChapter(highestCh + 1, false);
|
||||
setTimeout(() => { isAppendingChapter = false; }, 300);
|
||||
setTimeout(() => { isAppendingChapter = false; }, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3832,6 +3819,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
initReaderSettingsPanel();
|
||||
|
||||
if (readerContentBody) {
|
||||
readerContentBody.addEventListener('click', (e) => {
|
||||
const wordSpan = e.target.closest('.reader-word');
|
||||
if (wordSpan) {
|
||||
e.stopPropagation();
|
||||
const word = wordSpan.dataset.word;
|
||||
const context = wordSpan.dataset.context || wordSpan.closest('p, div, li, td, th, blockquote')?.textContent || word;
|
||||
handleWordClick(word, context, wordSpan);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (readerToggleHighlightBtn) {
|
||||
readerToggleHighlightBtn.addEventListener('click', () => {
|
||||
isHighlightEnabled = !isHighlightEnabled;
|
||||
|
||||
Reference in new issue
Block a user