Skip to content

Commit 27e3cdd

Browse files
silverwindlafriks
andauthored
Move syntax highlighting to web worker (#11017)
This should eliminate page freezes when loading big files/diff. `highlightBlock` is needed to preserve existing nodes when highlighting and for that, highlight.js needs access to the DOM API so I added a DOM implementation to make it work, which adds around 300kB to the output file size of the lazy-loaded `highlight.js`. Co-authored-by: Lauris BH <[email protected]>
1 parent cc4da79 commit 27e3cdd

File tree

7 files changed

+83
-17
lines changed

7 files changed

+83
-17
lines changed

.eslintrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ globals:
2424
SimpleMDE: false
2525
u2fApi: false
2626

27+
overrides:
28+
- files: ["web_src/**/*.worker.js"]
29+
env:
30+
worker: true
31+
rules:
32+
no-restricted-globals: [0]
33+
2734
rules:
2835
arrow-body-style: [0]
2936
arrow-parens: [2, always]

package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"core-js": "3.6.4",
1818
"css-loader": "3.4.2",
1919
"cssnano": "4.1.10",
20+
"domino": "2.1.4",
2021
"dropzone": "5.7.0",
2122
"fast-glob": "3.2.2",
2223
"fomantic-ui": "2.8.4",
@@ -44,7 +45,8 @@
4445
"vue-template-compiler": "2.6.11",
4546
"webpack": "4.42.0",
4647
"webpack-cli": "3.3.11",
47-
"webpack-fix-style-only-entries": "0.4.0"
48+
"webpack-fix-style-only-entries": "0.4.0",
49+
"worker-loader": "2.0.0"
4850
},
4951
"devDependencies": {
5052
"eslint": "6.8.0",

web_src/js/features/highlight.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
export default async function initHighlight() {
2-
if (!window.config || !window.config.HighlightJS) return;
1+
export default async function highlight(elementOrNodeList) {
2+
if (!window.config || !window.config.HighlightJS || !elementOrNodeList) return;
3+
const nodes = 'length' in elementOrNodeList ? elementOrNodeList : [elementOrNodeList];
4+
if (!nodes.length) return;
35

4-
const hljs = await import(/* webpackChunkName: "highlight" */'highlight.js');
6+
const {default: Worker} = await import(/* webpackChunkName: "highlight" */'./highlight.worker.js');
7+
const worker = new Worker();
58

6-
const nodes = [].slice.call(document.querySelectorAll('pre code') || []);
7-
for (let i = 0; i < nodes.length; i++) {
8-
hljs.highlightBlock(nodes[i]);
9-
}
9+
worker.addEventListener('message', ({data}) => {
10+
const {index, html} = data;
11+
nodes[index].outerHTML = html;
12+
});
1013

11-
return hljs;
14+
for (let index = 0; index < nodes.length; index++) {
15+
const node = nodes[index];
16+
if (!node) continue;
17+
worker.postMessage({index, html: node.outerHTML});
18+
}
1219
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {highlightBlock} from 'highlight.js';
2+
import {createWindow} from 'domino';
3+
4+
self.onmessage = function ({data}) {
5+
const window = createWindow();
6+
self.document = window.document;
7+
8+
const {index, html} = data;
9+
document.body.innerHTML = html;
10+
highlightBlock(document.body.firstChild);
11+
self.postMessage({index, html: document.body.innerHTML});
12+
};

web_src/js/index.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import './vendor/semanticdropdown.js';
1111
import {svg} from './utils.js';
1212

1313
import initContextPopups from './features/contextpopup.js';
14-
import initHighlight from './features/highlight.js';
1514
import initGitGraph from './features/gitgraph.js';
1615
import initClipboard from './features/clipboard.js';
1716
import initUserHeatmap from './features/userheatmap.js';
1817
import initDateTimePicker from './features/datetimepicker.js';
1918
import createDropzone from './features/dropzone.js';
19+
import highlight from './features/highlight.js';
2020
import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
2121

2222
const {AppSubUrl, StaticUrlPrefix, csrf} = window.config;
@@ -29,7 +29,6 @@ let previewFileModes;
2929
let simpleMDEditor;
3030
const commentMDEditors = {};
3131
let codeMirrorEditor;
32-
let hljs;
3332

3433
// Silence fomantic's error logging when tabs are used without a target content element
3534
$.fn.tab.settings.silent = true;
@@ -49,7 +48,7 @@ function initCommentPreviewTab($form) {
4948
$previewPanel.html(data);
5049
emojify.run($previewPanel[0]);
5150
$('pre code', $previewPanel[0]).each(function () {
52-
hljs.highlightBlock(this);
51+
highlight(this);
5352
});
5453
});
5554
});
@@ -75,7 +74,7 @@ function initEditPreviewTab($form) {
7574
$previewPanel.html(data);
7675
emojify.run($previewPanel[0]);
7776
$('pre code', $previewPanel[0]).each(function () {
78-
hljs.highlightBlock(this);
77+
highlight(this);
7978
});
8079
});
8180
});
@@ -1011,7 +1010,7 @@ async function initRepository() {
10111010
$renderContent.html(data.content);
10121011
emojify.run($renderContent[0]);
10131012
$('pre code', $renderContent[0]).each(function () {
1014-
hljs.highlightBlock(this);
1013+
highlight(this);
10151014
});
10161015
}
10171016
const $content = $segment.parent();
@@ -1337,7 +1336,7 @@ function initUncycloForm() {
13371336
preview.innerHTML = `<div class="markdown ui segment">${data}</div>`;
13381337
emojify.run($('.editor-preview')[0]);
13391338
$(preview).find('pre code').each((_, e) => {
1340-
hljs.highlightBlock(e);
1339+
highlight(e);
13411340
});
13421341
});
13431342
};
@@ -2633,8 +2632,8 @@ $(document).ready(async () => {
26332632
});
26342633

26352634
// parallel init of lazy-loaded features
2636-
[hljs] = await Promise.all([
2637-
initHighlight(),
2635+
await Promise.all([
2636+
highlight(document.querySelectorAll('pre code')),
26382637
initGitGraph(),
26392638
initClipboard(),
26402639
initUserHeatmap(),

webpack.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = {
5151
sourceMap: true,
5252
extractComments: false,
5353
terserOptions: {
54+
keep_fnames: /^(HTML|SVG)/, // https://github.com/fgnass/domino/issues/144
5455
output: {
5556
comments: false,
5657
},
@@ -89,6 +90,19 @@ module.exports = {
8990
test: require.resolve('jquery-datetimepicker'),
9091
use: 'imports-loader?define=>false,exports=>false',
9192
},
93+
{
94+
test: /\.worker\.js$/,
95+
use: [
96+
{
97+
loader: 'worker-loader',
98+
options: {
99+
name: '[name].js',
100+
inline: true,
101+
fallback: false,
102+
},
103+
},
104+
],
105+
},
92106
{
93107
test: /\.js$/,
94108
exclude: /node_modules/,

0 commit comments

Comments
 (0)