Commit 8658a2c7 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '328220-optimized-blob-view' into 'master'

Optimized blob view loading in repository

See merge request gitlab-org/gitlab!65859
parents 22b36201 d1e175f8
...@@ -6,6 +6,8 @@ import { ...@@ -6,6 +6,8 @@ import {
REPO_BLOB_LOAD_VIEWER_START, REPO_BLOB_LOAD_VIEWER_START,
REPO_BLOB_LOAD_VIEWER_FINISH, REPO_BLOB_LOAD_VIEWER_FINISH,
REPO_BLOB_LOAD_VIEWER, REPO_BLOB_LOAD_VIEWER,
REPO_BLOB_SWITCH_TO_VIEWER_START,
REPO_BLOB_SWITCH_VIEWER,
} from '~/performance/constants'; } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils'; import { performanceMarkAndMeasure } from '~/performance/utils';
import { fixTitle } from '~/tooltips'; import { fixTitle } from '~/tooltips';
...@@ -49,6 +51,9 @@ export const handleBlobRichViewer = (viewer, type) => { ...@@ -49,6 +51,9 @@ export const handleBlobRichViewer = (viewer, type) => {
export default class BlobViewer { export default class BlobViewer {
constructor() { constructor() {
performanceMarkAndMeasure({
mark: REPO_BLOB_LOAD_VIEWER_START,
});
const viewer = document.querySelector('.blob-viewer[data-type="rich"]'); const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
const type = viewer?.dataset?.richType; const type = viewer?.dataset?.richType;
BlobViewer.initAuxiliaryViewer(); BlobViewer.initAuxiliaryViewer();
...@@ -141,7 +146,7 @@ export default class BlobViewer { ...@@ -141,7 +146,7 @@ export default class BlobViewer {
switchToViewer(name) { switchToViewer(name) {
performanceMarkAndMeasure({ performanceMarkAndMeasure({
mark: REPO_BLOB_LOAD_VIEWER_START, mark: REPO_BLOB_SWITCH_TO_VIEWER_START,
}); });
const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`); const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return; if (this.activeViewer === newViewer) return;
...@@ -171,11 +176,15 @@ export default class BlobViewer { ...@@ -171,11 +176,15 @@ export default class BlobViewer {
BlobViewer.loadViewer(newViewer) BlobViewer.loadViewer(newViewer)
.then((viewer) => { .then((viewer) => {
$(viewer).renderGFM(); $(viewer).renderGFM();
window.requestIdleCallback(() => {
this.$fileHolder.trigger('highlight:line');
handleLocationHash();
this.$fileHolder.trigger('highlight:line'); viewer.setAttribute('data-loaded', 'true');
handleLocationHash(); this.toggleCopyButtonState();
eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
});
this.toggleCopyButtonState();
performanceMarkAndMeasure({ performanceMarkAndMeasure({
mark: REPO_BLOB_LOAD_VIEWER_FINISH, mark: REPO_BLOB_LOAD_VIEWER_FINISH,
measures: [ measures: [
...@@ -183,6 +192,10 @@ export default class BlobViewer { ...@@ -183,6 +192,10 @@ export default class BlobViewer {
name: REPO_BLOB_LOAD_VIEWER, name: REPO_BLOB_LOAD_VIEWER,
start: REPO_BLOB_LOAD_VIEWER_START, start: REPO_BLOB_LOAD_VIEWER_START,
}, },
{
name: REPO_BLOB_SWITCH_VIEWER,
start: REPO_BLOB_SWITCH_TO_VIEWER_START,
},
], ],
}); });
}) })
...@@ -205,9 +218,10 @@ export default class BlobViewer { ...@@ -205,9 +218,10 @@ export default class BlobViewer {
return axios.get(url).then(({ data }) => { return axios.get(url).then(({ data }) => {
viewer.innerHTML = data.html; viewer.innerHTML = data.html;
viewer.setAttribute('data-loaded', 'true');
eventHub.$emit('showBlobInteractionZones', viewer.dataset.path); window.requestIdleCallback(() => {
viewer.removeAttribute('data-loading');
});
return viewer; return viewer;
}); });
......
...@@ -83,7 +83,9 @@ export const PIPELINES_DETAIL_LINKS_JOB_RATIO = 'pipeline_graph_links_per_job_ra ...@@ -83,7 +83,9 @@ export const PIPELINES_DETAIL_LINKS_JOB_RATIO = 'pipeline_graph_links_per_job_ra
// Marks // Marks
export const REPO_BLOB_LOAD_VIEWER_START = 'blobviewer-load-viewer-start'; export const REPO_BLOB_LOAD_VIEWER_START = 'blobviewer-load-viewer-start';
export const REPO_BLOB_SWITCH_TO_VIEWER_START = 'blobviewer-switch-to-viewerr-start';
export const REPO_BLOB_LOAD_VIEWER_FINISH = 'blobviewer-load-viewer-finish'; export const REPO_BLOB_LOAD_VIEWER_FINISH = 'blobviewer-load-viewer-finish';
// Measures // Measures
export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the content'; export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the viewer';
export const REPO_BLOB_SWITCH_VIEWER = 'Repository File Viewer: switching the viewer';
...@@ -508,3 +508,25 @@ span.idiff { ...@@ -508,3 +508,25 @@ span.idiff {
} }
} }
} }
//
// IMPORTANT PERFORMANCE OPTIMIZATION BELOW
//
// * :nth-of-type(1n+70) - makes sure we do not render lines 71+ right
// away. Even though the HTML is injected in the DOM, as long as we do
// not render those lines, the browser doesn't need to spend resources
// calculating and repainting what's hidden.
//
// * :not(:last-of-type) makes sure that we output the last line of the
// blob's snippet. This is important because the column with the line
// numbers has auto width and is expanding based on the content in it.
// This leads to unnecessary layout shift when the last lines of the
// snippet are longer than two (2) digits.
// EXAMPLE: Let's say, we have a blob with 100 lines. If we output 70
// lines, and then, the remaining 30 (incl the line 100), it will lead
// to the layout reflow and styles recalculation when we output line
// 100 (because the width of '100' is always bigger than '70'). By
// outputting the last line right away, we prevent that as the column
// will always be expanded to the maximum needed width.
.blob-viewer[data-loading] .file-content.code .line:nth-of-type(1n+70):not(:last-of-type),
.blob-viewer[data-loading] .file-content.code .file-line-num:nth-of-type(1n+70):not(:last-of-type) {display: none !important;}
...@@ -6,6 +6,10 @@ import { setTestTimeout } from 'helpers/timeout'; ...@@ -6,6 +6,10 @@ import { setTestTimeout } from 'helpers/timeout';
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
const execImmediately = (callback) => {
callback();
};
describe('Blob viewer', () => { describe('Blob viewer', () => {
let blob; let blob;
let mock; let mock;
...@@ -17,6 +21,7 @@ describe('Blob viewer', () => { ...@@ -17,6 +21,7 @@ describe('Blob viewer', () => {
setTestTimeout(2000); setTestTimeout(2000);
beforeEach(() => { beforeEach(() => {
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
$.fn.extend(jQueryMock); $.fn.extend(jQueryMock);
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment