summaryrefslogtreecommitdiff
path: root/doc/development/fe_guide/performance.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/development/fe_guide/performance.md')
-rw-r--r--doc/development/fe_guide/performance.md272
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.