From 27e3cddfbef533f783898800ac4abdd5a453b436 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 13 Apr 2020 15:02:31 +0200 Subject: [PATCH] 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 --- .eslintrc | 7 +++++++ package-lock.json | 25 +++++++++++++++++++++++++ package.json | 4 +++- web_src/js/features/highlight.js | 23 +++++++++++++++-------- web_src/js/features/highlight.worker.js | 12 ++++++++++++ web_src/js/index.js | 15 +++++++-------- webpack.config.js | 14 ++++++++++++++ 7 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 web_src/js/features/highlight.worker.js diff --git a/.eslintrc b/.eslintrc index 76e6f8c48..8fd53d54a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,6 +24,13 @@ globals: SimpleMDE: false u2fApi: false +overrides: + - files: ["web_src/**/*.worker.js"] + env: + worker: true + rules: + no-restricted-globals: [0] + rules: arrow-body-style: [0] arrow-parens: [2, always] diff --git a/package-lock.json b/package-lock.json index 6f1f73245..f0f7d0801 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3659,6 +3659,11 @@ "domelementtype": "1" } }, + "domino": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.4.tgz", + "integrity": "sha512-l70mlQ7IjPKC8kT7GljQXJZmt5OqFL+RE91ik5y5WWQtsd9wP8R7gpFnNu96fK5MqAAZRXfLLsnzKtkty5fWGQ==" + }, "dompurify": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.0.8.tgz", @@ -15008,6 +15013,26 @@ "errno": "~0.1.7" } }, + "worker-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", + "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", + "requires": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + }, + "dependencies": { + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", diff --git a/package.json b/package.json index abb04356a..88360b6b6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "core-js": "3.6.4", "css-loader": "3.4.2", "cssnano": "4.1.10", + "domino": "2.1.4", "dropzone": "5.7.0", "fast-glob": "3.2.2", "fomantic-ui": "2.8.4", @@ -44,7 +45,8 @@ "vue-template-compiler": "2.6.11", "webpack": "4.42.0", "webpack-cli": "3.3.11", - "webpack-fix-style-only-entries": "0.4.0" + "webpack-fix-style-only-entries": "0.4.0", + "worker-loader": "2.0.0" }, "devDependencies": { "eslint": "6.8.0", diff --git a/web_src/js/features/highlight.js b/web_src/js/features/highlight.js index dcd8a8d21..d3f6ba71b 100644 --- a/web_src/js/features/highlight.js +++ b/web_src/js/features/highlight.js @@ -1,12 +1,19 @@ -export default async function initHighlight() { - if (!window.config || !window.config.HighlightJS) return; +export default async function highlight(elementOrNodeList) { + if (!window.config || !window.config.HighlightJS || !elementOrNodeList) return; + const nodes = 'length' in elementOrNodeList ? elementOrNodeList : [elementOrNodeList]; + if (!nodes.length) return; - const hljs = await import(/* webpackChunkName: "highlight" */'highlight.js'); + const {default: Worker} = await import(/* webpackChunkName: "highlight" */'./highlight.worker.js'); + const worker = new Worker(); - const nodes = [].slice.call(document.querySelectorAll('pre code') || []); - for (let i = 0; i < nodes.length; i++) { - hljs.highlightBlock(nodes[i]); - } + worker.addEventListener('message', ({data}) => { + const {index, html} = data; + nodes[index].outerHTML = html; + }); - return hljs; + for (let index = 0; index < nodes.length; index++) { + const node = nodes[index]; + if (!node) continue; + worker.postMessage({index, html: node.outerHTML}); + } } diff --git a/web_src/js/features/highlight.worker.js b/web_src/js/features/highlight.worker.js new file mode 100644 index 000000000..7d6cc4e43 --- /dev/null +++ b/web_src/js/features/highlight.worker.js @@ -0,0 +1,12 @@ +import {highlightBlock} from 'highlight.js'; +import {createWindow} from 'domino'; + +self.onmessage = function ({data}) { + const window = createWindow(); + self.document = window.document; + + const {index, html} = data; + document.body.innerHTML = html; + highlightBlock(document.body.firstChild); + self.postMessage({index, html: document.body.innerHTML}); +}; diff --git a/web_src/js/index.js b/web_src/js/index.js index 6476b2cfb..63a5134bb 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -11,12 +11,12 @@ import './vendor/semanticdropdown.js'; import {svg} from './utils.js'; import initContextPopups from './features/contextpopup.js'; -import initHighlight from './features/highlight.js'; import initGitGraph from './features/gitgraph.js'; import initClipboard from './features/clipboard.js'; import initUserHeatmap from './features/userheatmap.js'; import initDateTimePicker from './features/datetimepicker.js'; import createDropzone from './features/dropzone.js'; +import highlight from './features/highlight.js'; import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; @@ -29,7 +29,6 @@ let previewFileModes; let simpleMDEditor; const commentMDEditors = {}; let codeMirrorEditor; -let hljs; // Silence fomantic's error logging when tabs are used without a target content element $.fn.tab.settings.silent = true; @@ -49,7 +48,7 @@ function initCommentPreviewTab($form) { $previewPanel.html(data); emojify.run($previewPanel[0]); $('pre code', $previewPanel[0]).each(function () { - hljs.highlightBlock(this); + highlight(this); }); }); }); @@ -75,7 +74,7 @@ function initEditPreviewTab($form) { $previewPanel.html(data); emojify.run($previewPanel[0]); $('pre code', $previewPanel[0]).each(function () { - hljs.highlightBlock(this); + highlight(this); }); }); }); @@ -1011,7 +1010,7 @@ async function initRepository() { $renderContent.html(data.content); emojify.run($renderContent[0]); $('pre code', $renderContent[0]).each(function () { - hljs.highlightBlock(this); + highlight(this); }); } const $content = $segment.parent(); @@ -1337,7 +1336,7 @@ function initWikiForm() { preview.innerHTML = `
${data}
`; emojify.run($('.editor-preview')[0]); $(preview).find('pre code').each((_, e) => { - hljs.highlightBlock(e); + highlight(e); }); }); }; @@ -2633,8 +2632,8 @@ $(document).ready(async () => { }); // parallel init of lazy-loaded features - [hljs] = await Promise.all([ - initHighlight(), + await Promise.all([ + highlight(document.querySelectorAll('pre code')), initGitGraph(), initClipboard(), initUserHeatmap(), diff --git a/webpack.config.js b/webpack.config.js index 57a41a11a..77680cb37 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -51,6 +51,7 @@ module.exports = { sourceMap: true, extractComments: false, terserOptions: { + keep_fnames: /^(HTML|SVG)/, // https://github.com/fgnass/domino/issues/144 output: { comments: false, }, @@ -89,6 +90,19 @@ module.exports = { test: require.resolve('jquery-datetimepicker'), use: 'imports-loader?define=>false,exports=>false', }, + { + test: /\.worker\.js$/, + use: [ + { + loader: 'worker-loader', + options: { + name: '[name].js', + inline: true, + fallback: false, + }, + }, + ], + }, { test: /\.js$/, exclude: /node_modules/,