diff options
Diffstat (limited to 'doc/development/fe_guide/performance.md')
-rw-r--r-- | doc/development/fe_guide/performance.md | 272 |
1 files changed, 233 insertions, 39 deletions
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md index de9a9f5cb14..7825c89b7cf 100644 --- a/doc/development/fe_guide/performance.md +++ b/doc/development/fe_guide/performance.md @@ -1,11 +1,205 @@ --- stage: none group: unassigned -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Performance +Performance is an essential part and one of the main areas of concern for any modern application. + +## User Timing API + +[User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API) is a web API +[available in all modern browsers](https://caniuse.com/?search=User%20timing). It allows measuring +custom times and durations in your applications by placing special marks in your +code. You can use the User Timing API in GitLab to measure any timing, regardless of the framework, +including Rails, Vue, or vanilla JavaScript environments. For consistency and +convenience of adoption, GitLab offers several ways to enable custom user timing metrics in +your code. + +User Timing API introduces two important paradigms: `mark` and `measure`. + +**Mark** is the timestamp on the performance timeline. For example, +`performance.mark('my-component-start');` makes a browser note the time this code +is met. Then, you can obtain information about this mark by querying the global +performance object again. For example, in your DevTools console: + +```javascript +performance.getEntriesByName('my-component-start') +``` + +**Measure** is the duration between either: + +- Two marks +- The start of navigation and a mark +- The start of navigation and the moment the measurement is taken + +It takes several arguments of which the measurement’s name is the only one required. Examples: + +- Duration between the start and end marks: + + ```javascript + performance.measure('My component', 'my-component-start', 'my-component-end') + ``` + +- Duration between a mark and the moment the measurement is taken. The end mark is omitted in + this case. + + ```javascript + performance.measure('My component', 'my-component-start') + ``` + +- Duration between [the navigation start](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin) + and the moment the actual measurement is taken. + + ```javascript + performance.measure('My component') + ``` + +- Duration between [the navigation start](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin) + and a mark. You cannot omit the start mark in this case but you can set it to `undefined`. + + ```javascript + performance.measure('My component', undefined, 'my-component-end') + ``` + +To query a particular `measure`, You can use the same API, as for `mark`: + +```javascript +performance.getEntriesByName('My component') +``` + +You can also query for all captured marks and measurements: + +```javascript +performance.getEntriesByType('mark'); +performance.getEntriesByType('measure'); +``` + +Using `getEntriesByName()` or `getEntriesByType()` returns an Array of [the PerformanceMeasure +objects](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure) which contain +information about the measurement's start time and duration. + +### User Timing API utility + +You can use the `performanceMarkAndMeasure` utility anywhere in GitLab, as it's not tied to any +particular environment. + +`performanceMarkAndMeasure` takes an object as an argument, where: + +| Attribute | Type | Required | Description | +|:------------|:---------|:---------|:----------------------| +| `mark` | `String` | no | The name for the mark to set. Used for retrieving the mark later. If not specified, the mark is not set. | +| `measures` | `Array` | no | The list of the measurements to take at this point. | + +In return, the entries in the `measures` array are objects with the following API: + +| Attribute | Type | Required | Description | +|:------------|:---------|:---------|:----------------------| +| `name` | `String` | yes | The name for the measurement. Used for retrieving the mark later. Must be specified for every measure object, otherwise JavaScript fails. | +| `start` | `String` | no | The name of a mark **from** which the measurement should be taken. | +| `end` | `String` | no | The name of a mark **to** which the measurement should be taken. | + +Example: + +```javascript +import { performanceMarkAndMeasure } from '~/performance/utils'; +... +performanceMarkAndMeasure({ + mark: MR_DIFFS_MARK_DIFF_FILES_END, + measures: [ + { + name: MR_DIFFS_MEASURE_DIFF_FILES_DONE, + start: MR_DIFFS_MARK_DIFF_FILES_START, + end: MR_DIFFS_MARK_DIFF_FILES_END, + }, + ], +}); +``` + +### Vue performance plugin + +The plugin captures and measures the performance of the specified Vue components automatically +leveraging the Vue lifecycle and the User Timing API. + +To use the Vue performance plugin: + +1. Import the plugin: + + ```javascript + import PerformancePlugin from '~/performance/vue_performance_plugin'; + ``` + +1. Use it before initializing your Vue application: + + ```javascript + Vue.use(PerformancePlugin, { + components: [ + 'IdeTreeList', + 'FileTree', + 'RepoEditor', + ] + }); + ``` + +The plugin accepts the list of components, performance of which should be measured. The components +should be specified by their `name` option. + +You might need to explicitly set this option on the needed components, as +most components in the codebase don't have this option set: + +```javascript +export default { + name: 'IdeTreeList', + components: { + ... + ... +} +``` + +The plugin captures and stores the following: + +- The start **mark** for when the component has been initialized (in `beforeCreate()` hook) +- The end **mark** of the component when it has been rendered (next animation frame after `nextTick` + in `mounted()` hook). In most cases, this event does not wait for all sub-components to be + bootstrapped. To measure the sub-components, you should include those into the + plugin options. +- **Measure** duration between the two marks above. + +### Access stored measurements + +To access stored measurements, you can use either: + +- **Performance bar**. If you have it enabled (`P` + `B` key-combo), you can see the metrics + output in your DevTools console. +- **"Performance" tab** of the DevTools. You can get the measurements (not the marks, though) in + this tab when profiling performance. +- **DevTools console**. As mentioned above, you can query for the entries: + + ```javascript + performance.getEntriesByType('mark'); + performance.getEntriesByType('measure'); + ``` + +### Naming convention + +All the marks and measures should be instantiated with the constants from +`app/assets/javascripts/performance/constants.js`. When you’re ready to add a new mark’s or +measurement’s label, you can follow the pattern. + +NOTE: +This pattern is a recommendation and not a hard rule. + +```javascript +app-*-start // for a start ‘mark’ +app-*-end // for an end ‘mark’ +app-* // for ‘measure’ +``` + +For example, `'webide-init-editor-start`, `mr-diffs-mark-file-tree-end`, and so on. We do it to +help identify marks and measures coming from the different apps on the same page. + ## Best Practices ### Realtime Components @@ -18,29 +212,29 @@ When writing code for realtime features we have to keep a couple of things in mi Thus, we must strike a balance between sending requests and the feeling of realtime. Use the following rules when creating realtime solutions. -1. The server will tell you how much to poll by sending `Poll-Interval` in the header. - Use that as your polling interval. This way it is [easy for system administrators to change the - polling rate](../../administration/polling.md). +1. The server tells you how much to poll by sending `Poll-Interval` in the header. + Use that as your polling interval. This enables system administrators to change the + [polling rate](../../administration/polling.md). A `Poll-Interval: -1` means you should disable polling, and this must be implemented. 1. A response with HTTP status different from 2XX should disable polling as well. 1. Use a common library for polling. 1. Poll on active tabs only. Please use [Visibility](https://github.com/ai/visibilityjs). -1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be +1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval is controlled by the server. -1. The backend code will most likely be using etags. You do not and should not check for status - `304 Not Modified`. The browser will transform it for you. +1. The backend code is likely to be using etags. You do not and should not check for status + `304 Not Modified`. The browser transforms it for you. ### Lazy Loading Images To improve the time to first render we are using lazy loading for images. This works by setting the actual image source on the `data-src` attribute. After the HTML is rendered and JavaScript is loaded, -the value of `data-src` will be moved to `src` automatically if the image is in the current viewport. +the value of `data-src` is moved to `src` automatically if the image is in the current viewport. - Prepare images in HTML for lazy loading by renaming the `src` attribute to `data-src` AND adding the class `lazy`. -- If you are using the Rails `image_tag` helper, all images will be lazy-loaded by default unless `lazy: false` is provided. +- If you are using the Rails `image_tag` helper, all images are lazy-loaded by default unless `lazy: false` is provided. If you are asynchronously adding content which contains lazy images then you need to call the function -`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed. +`gl.lazyLoader.searchLazyImages()` which searches for lazy images and loads them if needed. But in general it should be handled automatically through a `MutationObserver` in the lazy loading function. ### Animations @@ -49,14 +243,14 @@ Only animate `opacity` & `transform` properties. Other properties (such as `top` Layout to be recalculated, which is much more expensive. For details on this, see "Styles that Affect Layout" in [High Performance Animations](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/). -If you _do_ need to change layout (e.g. a sidebar that pushes main content over), prefer [FLIP](https://aerotwist.com/blog/flip-your-animations/) to change expensive +If you _do_ need to change layout (for example, a sidebar that pushes main content over), prefer [FLIP](https://aerotwist.com/blog/flip-your-animations/) to change expensive properties once, and handle the actual animation with transforms. ## Reducing Asset Footprint ### Universal code -Code that is contained within `main.js` and `commons/index.js` are loaded and +Code that is contained in `main.js` and `commons/index.js` is loaded and run on _all_ pages. **DO NOT ADD** anything to these files unless it is truly needed _everywhere_. These bundles include ubiquitous libraries like `vue`, `axios`, and `jQuery`, as well as code for the main navigation and sidebar. @@ -66,26 +260,26 @@ code footprint. ### Page-specific JavaScript Webpack has been configured to automatically generate entry point bundles based -on the file structure within `app/assets/javascripts/pages/*`. The directories -within the `pages` directory correspond to Rails controllers and actions. These -auto-generated bundles will be automatically included on the corresponding +on the file structure in `app/assets/javascripts/pages/*`. The directories +in the `pages` directory correspond to Rails controllers and actions. These +auto-generated bundles are automatically included on the corresponding pages. For example, if you were to visit <https://gitlab.com/gitlab-org/gitlab/-/issues>, you would be accessing the `app/controllers/projects/issues_controller.rb` controller with the `index` action. If a corresponding file exists at -`pages/projects/issues/index/index.js`, it will be compiled into a webpack +`pages/projects/issues/index/index.js`, it is compiled into a webpack bundle and included on the page. Previously, GitLab encouraged the use of -`content_for :page_specific_javascripts` within HAML files, along with +`content_for :page_specific_javascripts` in HAML files, along with manually generated webpack bundles. However under this new system you should not ever need to manually add an entry point to the `webpack.config.js` file. -TIP: **Tip:** +NOTE: If you are unsure what controller and action corresponds to a given page, you -can find this out by inspecting `document.body.dataset.page` within your -browser's developer console while on any page within GitLab. +can find this out by inspecting `document.body.dataset.page` in your +browser's developer console while on any page in GitLab. #### Important Considerations @@ -97,7 +291,7 @@ browser's developer console while on any page within GitLab. instantiate, and nothing else. - **`DOMContentLoaded` should not be used:** - All of GitLab's JavaScript files are added with the `defer` attribute. + All GitLab JavaScript files are added with the `defer` attribute. According to the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer), this implies that "the script is meant to be executed after the document has been parsed, but before firing `DOMContentLoaded`". Since the document is already @@ -119,21 +313,21 @@ browser's developer console while on any page within GitLab. ``` Note that `waitForCSSLoaded()` methods supports receiving the action in different ways: - + - With a callback: - + ```javascript waitForCSSLoaded(action) ``` - + - With `then()`: - + ```javascript waitForCSSLoaded().then(action); ``` - + - With `await` followed by `action`: - + ```javascript await waitForCSSLoaded; action(); @@ -150,21 +344,21 @@ browser's developer console while on any page within GitLab. - **Supporting Module Placement:** - If a class or a module is _specific to a particular route_, try to locate - it close to the entry point it will be used. For instance, if - `my_widget.js` is only imported within `pages/widget/show/index.js`, you + it close to the entry point in which it is used. For instance, if + `my_widget.js` is only imported in `pages/widget/show/index.js`, you should place the module at `pages/widget/show/my_widget.js` and import it - with a relative path (e.g. `import initMyWidget from './my_widget';`). - - If a class or module is _used by multiple routes_, place it within a + with a relative path (for example, `import initMyWidget from './my_widget';`). + - If a class or module is _used by multiple routes_, place it in a shared directory at the closest common parent directory for the entry - points that import it. For example, if `my_widget.js` is imported within + points that import it. For example, if `my_widget.js` is imported in both `pages/widget/show/index.js` and `pages/widget/run/index.js`, then place the module at `pages/widget/shared/my_widget.js` and import it with - a relative path if possible (e.g. `../shared/my_widget`). + a relative path if possible (for example, `../shared/my_widget`). - **Enterprise Edition Caveats:** - For GitLab Enterprise Edition, page-specific entry points will override their + For GitLab Enterprise Edition, page-specific entry points override their Community Edition counterparts with the same name, so if - `ee/app/assets/javascripts/pages/foo/bar/index.js` exists, it will take + `ee/app/assets/javascripts/pages/foo/bar/index.js` exists, it takes precedence over `app/assets/javascripts/pages/foo/bar/index.js`. If you want to minimize duplicate code, you can import one entry point from the other. This is not done automatically to allow for flexibility in overriding @@ -172,10 +366,10 @@ browser's developer console while on any page within GitLab. ### Code Splitting -For any code that does not need to be run immediately upon page load, (e.g. +For any code that does not need to be run immediately upon page load, (for example, modals, dropdowns, and other behaviors that can be lazy-loaded), you can split your module into asynchronous chunks with dynamic import statements. These -imports return a Promise which will be resolved once the script has loaded: +imports return a Promise which is resolved after the script has loaded: ```javascript import(/* webpackChunkName: 'emoji' */ '~/emoji') @@ -184,7 +378,7 @@ import(/* webpackChunkName: 'emoji' */ '~/emoji') ``` Please try to use `webpackChunkName` when generating these dynamic imports as -it will provide a deterministic filename for the chunk which can then be cached +it provides a deterministic filename for the chunk which can then be cached the browser across GitLab versions. More information is available in [webpack's code splitting documentation](https://webpack.js.org/guides/code-splitting/#dynamic-imports). @@ -198,7 +392,7 @@ data is used for users with capped data plans. General tips: - Don't add new fonts. -- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF. +- Prefer font formats with better compression, for example, WOFF2 is better than WOFF, which is better than TTF. - Compress and minify assets wherever possible (For CSS/JS, Sprockets and webpack do this for us). - If some functionality can reasonably be achieved without adding extra libraries, avoid them. - Use page-specific JavaScript as described above to load libraries that are only needed on certain pages. |