Skip to content

Commit ff373f7

Browse files
perf: optimize loading, resolve layout thrashing typing lag, and add service worker
1 parent 04736e5 commit ff373f7

3 files changed

Lines changed: 143 additions & 27 deletions

File tree

index.html

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<!-- DNS Prefetch & Preconnect CDN Origins to Warm Up Latency -->
7+
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
8+
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
9+
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
10+
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
11+
612
<!-- Primary Meta Tags -->
713
<meta name="title" content="Markdown Viewer">
814
<meta name="description" content="Markdown Viewer is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
@@ -36,12 +42,11 @@
3642
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css" integrity="sha384-4ok+tBQQdy5hcPT56tzcE11yQ2BkN0Py1uDE8ZOiXYstHOpUB61pJafm+NidByp4" crossorigin="anonymous">
3743
<link rel="stylesheet" href="styles.css">
3844

39-
<!-- Loading order optimized - ensure libraries are loaded before they're used -->
40-
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js" integrity="sha384-odPBjvtXVM/5hOYIr3A1dB+flh0c3wAT3bSesIOqEGmyUA4JoKf/YTWy0XKOYAY7" crossorigin="anonymous"></script>
41-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous"></script>
42-
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js" integrity="sha384-3HPB1XT51W3gGRxAmZ+qbZwRpRlFQL632y8x+adAqCr4Wp3TaWwCLSTAJJKbyWEK" crossorigin="anonymous"></script>
43-
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" integrity="sha384-PlRSzpewlarQuj5alIadXwjNUX+2eNMKwr0f07ShWYLy8B6TjEbm7ZlcN/ScSbwy" crossorigin="anonymous"></script>
44-
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" integrity="sha384-Yv5O+t3uE3hunW8uyrbpPW3iw6/5/Y7HitWJBLgqfMoA36NogMmy+8wWZMpn3HWc" crossorigin="anonymous"></script>
45+
<!-- Loading order optimized - ensure libraries are loaded asynchronously using defer -->
46+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js" integrity="sha384-odPBjvtXVM/5hOYIr3A1dB+flh0c3wAT3bSesIOqEGmyUA4JoKf/YTWy0XKOYAY7" crossorigin="anonymous" defer></script>
47+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous" defer></script>
48+
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js" integrity="sha384-3HPB1XT51W3gGRxAmZ+qbZwRpRlFQL632y8x+adAqCr4Wp3TaWwCLSTAJJKbyWEK" crossorigin="anonymous" defer></script>
49+
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" integrity="sha384-PlRSzpewlarQuj5alIadXwjNUX+2eNMKwr0f07ShWYLy8B6TjEbm7ZlcN/ScSbwy" crossorigin="anonymous" defer></script>
4550
<script>
4651
window.MathJax = {
4752
loader: { load: ['[tex]/ams', '[tex]/boldsymbol'] },
@@ -53,16 +58,13 @@
5358
}
5459
};
5560
</script>
56-
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js" integrity="sha384-M5jmNxKC9EVnuqeMwRHvFuYUE8Hhp0TgBruj/GZRkYtiMrCRgH7yvv5KY+Owi7TW" crossorigin="anonymous"></script>
57-
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js" integrity="sha384-zkWMJO4sgpPUzyuOgDx8HB/K55glbAwajEpk1Go2NWRuPkPA/wIhoEJTuSkmOYrV" crossorigin="anonymous"></script>
58-
<script src="https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js" integrity="sha384-1+n1eMmP5I08CibRJ6JmycJ0hP3G6C0fuUtTb4bEuQgl9uFdS9pnPePfpmrXl9ll" crossorigin="anonymous"></script>
59-
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js" integrity="sha384-JcnsjUPPylna1s1fvi1u12X5qjY5OL56iySh75FdtrwhO/SWXgMjoVqcKyIIWOLk" crossorigin="anonymous"></script>
60-
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" integrity="sha384-ZZ1pncU3bQe8y31yfZdMFdSpttDoPmOZg2wguVK9almUodir1PghgT0eY7Mrty8H" crossorigin="anonymous"></script>
61-
62-
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js" integrity="sha384-VFQrHzqBh5qiJIU0uGU5CIW3+OWpdGGJM9LBnGbuIH2mkICcFZ7lPd/AAtI7SNf7" crossorigin="anonymous"></script>
63-
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js" integrity="sha384-/RlQG9uf0M2vcTw3CX7fbqgbj/h8wKxw7C3zu9/GxcBPRKOEcESxaxufwRXqzq6n" crossorigin="anonymous"></script>
64-
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js" integrity="sha384-rNlaE5fs9dGIjmxWDALQh/RBAaGRYT5ChrzHo6tRfgrZ36iRFAiquP5g41Jsv+0j" crossorigin="anonymous"></script>
65-
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js" integrity="sha384-+pxiN6T7yvpryuJmE1gM9PX7yQit15auDb+ZwwvJOd/4be2Cie5/IuVXgQb/S9du" crossorigin="anonymous"></script>
61+
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js" integrity="sha384-M5jmNxKC9EVnuqeMwRHvFuYUE8Hhp0TgBruj/GZRkYtiMrCRgH7yvv5KY+Owi7TW" crossorigin="anonymous" defer></script>
62+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js" integrity="sha384-zkWMJO4sgpPUzyuOgDx8HB/K55glbAwajEpk1Go2NWRuPkPA/wIhoEJTuSkmOYrV" crossorigin="anonymous" defer></script>
63+
<script src="https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js" integrity="sha384-1+n1eMmP5I08CibRJ6JmycJ0hP3G6C0fuUtTb4bEuQgl9uFdS9pnPePfpmrXl9ll" crossorigin="anonymous" defer></script>
64+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js" integrity="sha384-JcnsjUPPylna1s1fvi1u12X5qjY5OL56iySh75FdtrwhO/SWXgMjoVqcKyIIWOLk" crossorigin="anonymous" defer></script>
65+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" integrity="sha384-ZZ1pncU3bQe8y31yfZdMFdSpttDoPmOZg2wguVK9almUodir1PghgT0eY7Mrty8H" crossorigin="anonymous" defer></script>
66+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js" integrity="sha384-rNlaE5fs9dGIjmxWDALQh/RBAaGRYT5ChrzHo6tRfgrZ36iRFAiquP5g41Jsv+0j" crossorigin="anonymous" defer></script>
67+
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js" integrity="sha384-+pxiN6T7yvpryuJmE1gM9PX7yQit15auDb+ZwwvJOd/4be2Cie5/IuVXgQb/S9du" crossorigin="anonymous" defer></script>
6668
</head>
6769
<body>
6870
<div class="app-container">

script.js

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3631,36 +3631,68 @@ This is a fully client-side application. Your content never leaves your browser
36313631
return Math.max(1, Math.round(contentHeight / lineHeight));
36323632
}
36333633

3634+
const lineCache = new Map();
3635+
let lastEditorWidth = 0;
3636+
36343637
function updateLineNumbers() {
36353638
if (!lineNumbers || !markdownEditor) return;
36363639
const lines = (markdownEditor.value || '').split('\n');
36373640
const lineCount = Math.max(1, lines.length);
3641+
3642+
// Clear height cache if editor width has changed
3643+
const currentWidth = markdownEditor.clientWidth;
3644+
if (currentWidth !== lastEditorWidth) {
3645+
lineCache.clear();
3646+
lastEditorWidth = currentWidth;
3647+
}
3648+
36383649
updateLineNumberGutter(lineCount);
36393650
ensureLineNumberMeasure();
36403651
const styles = window.getComputedStyle(markdownEditor);
36413652
const lineHeight = getLineHeight(styles);
36423653
const paddingSum =
36433654
(parseFloat(styles.paddingTop) || 0) +
36443655
(parseFloat(styles.paddingBottom) || 0);
3656+
36453657
const existingItems = lineNumbers.children;
3646-
if (existingItems.length !== lineCount) {
3658+
const existingCount = existingItems.length;
3659+
3660+
// Adjust the number of DOM elements in-place to avoid complete tear-down
3661+
if (existingCount < lineCount) {
36473662
const fragment = document.createDocumentFragment();
3648-
lines.forEach(function(line, index) {
3663+
for (let i = existingCount; i < lineCount; i += 1) {
36493664
const lineNumber = document.createElement('div');
36503665
lineNumber.className = 'line-number';
3651-
lineNumber.textContent = index + 1;
3652-
const wrapCount = getWrappedLineCount(line, lineHeight, paddingSum);
3653-
lineNumber.style.height = `${wrapCount * lineHeight}px`;
36543666
fragment.appendChild(lineNumber);
3655-
});
3656-
lineNumbers.textContent = '';
3667+
}
36573668
lineNumbers.appendChild(fragment);
3658-
} else {
3659-
for (let i = 0; i < lineCount; i += 1) {
3660-
const wrapCount = getWrappedLineCount(lines[i], lineHeight, paddingSum);
3661-
existingItems[i].style.height = `${wrapCount * lineHeight}px`;
3669+
} else if (existingCount > lineCount) {
3670+
while (lineNumbers.children.length > lineCount) {
3671+
lineNumbers.removeChild(lineNumbers.lastChild);
3672+
}
3673+
}
3674+
3675+
// Update only the heights and numbers that changed, querying cache
3676+
for (let i = 0; i < lineCount; i += 1) {
3677+
const lineText = lines[i];
3678+
let wrapHeight = lineCache.get(lineText);
3679+
if (wrapHeight === undefined) {
3680+
const wrapCount = getWrappedLineCount(lineText, lineHeight, paddingSum);
3681+
wrapHeight = wrapCount * lineHeight;
3682+
lineCache.set(lineText, wrapHeight);
3683+
}
3684+
3685+
const item = existingItems[i];
3686+
const targetText = String(i + 1);
3687+
if (item.textContent !== targetText) {
3688+
item.textContent = targetText;
3689+
}
3690+
const targetHeight = `${wrapHeight}px`;
3691+
if (item.style.height !== targetHeight) {
3692+
item.style.height = targetHeight;
36623693
}
36633694
}
3695+
36643696
syncLineNumberScroll();
36653697
}
36663698

@@ -5868,4 +5900,15 @@ This is a fully client-side application. Your content never leaves your browser
58685900
container.appendChild(toolbar);
58695901
});
58705902
}
5903+
5904+
// Register Service Worker for offline capabilities
5905+
if ('serviceWorker' in navigator) {
5906+
window.addEventListener('load', function() {
5907+
navigator.serviceWorker.register('sw.js').then(function(registration) {
5908+
console.log('ServiceWorker registration successful with scope: ', registration.scope);
5909+
}, function(err) {
5910+
console.log('ServiceWorker registration failed: ', err);
5911+
});
5912+
});
5913+
}
58715914
});

sw.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const CACHE_NAME = 'markdown-viewer-cache-v1';
2+
const ASSETS = [
3+
'./',
4+
'./index.html',
5+
'./script.js',
6+
'./styles.css',
7+
'./assets/icon.jpg',
8+
// CDN Stylesheets
9+
'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css',
10+
'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css',
11+
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css',
12+
'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css',
13+
// CDN Scripts
14+
'https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js',
15+
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js',
16+
'https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js',
17+
'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js',
18+
'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js',
19+
'https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js',
20+
'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js',
21+
'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
22+
'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
23+
'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js',
24+
'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js',
25+
'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js'
26+
];
27+
28+
self.addEventListener('install', event => {
29+
event.waitUntil(
30+
caches.open(CACHE_NAME)
31+
.then(cache => cache.addAll(ASSETS))
32+
.then(() => self.skipWaiting())
33+
);
34+
});
35+
36+
self.addEventListener('activate', event => {
37+
event.waitUntil(
38+
caches.keys().then(keys => Promise.all(
39+
keys.map(key => {
40+
if (key !== CACHE_NAME) {
41+
return caches.delete(key);
42+
}
43+
})
44+
)).then(() => self.clients.claim())
45+
);
46+
});
47+
48+
self.addEventListener('fetch', event => {
49+
event.respondWith(
50+
caches.match(event.request)
51+
.then(cachedResponse => {
52+
if (cachedResponse) {
53+
return cachedResponse;
54+
}
55+
return fetch(event.request).then(response => {
56+
// Cache newly requested resources if they are from safe origins
57+
if (response && response.status === 200 && (
58+
event.request.url.startsWith(self.location.origin) ||
59+
event.request.url.includes('cdnjs.cloudflare.com') ||
60+
event.request.url.includes('cdn.jsdelivr.net')
61+
)) {
62+
const responseToCache = response.clone();
63+
caches.open(CACHE_NAME).then(cache => {
64+
cache.put(event.request, responseToCache);
65+
});
66+
}
67+
return response;
68+
});
69+
})
70+
);
71+
});

0 commit comments

Comments
 (0)