410 lines
17 KiB
Markdown
410 lines
17 KiB
Markdown
---
|
||
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/#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
|
||
|
||
### Real-time Components
|
||
|
||
When writing code for real-time features we have to keep a couple of things in mind:
|
||
|
||
1. Do not overload the server with requests.
|
||
1. It should feel real-time.
|
||
|
||
Thus, we must strike a balance between sending requests and the feeling of real-time.
|
||
Use the following rules when creating real-time solutions.
|
||
|
||
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 is
|
||
controlled by the server.
|
||
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` 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 are lazy-loaded by default unless `lazy: false` is provided.
|
||
|
||
When asynchronously adding content which contains lazy images, call the function
|
||
`gl.lazyLoader.searchLazyImages()` which searches for lazy images and loads them if needed.
|
||
In general, it should be handled automatically through a `MutationObserver` in the lazy loading function.
|
||
|
||
### Animations
|
||
|
||
Only animate `opacity` & `transform` properties. Other properties (such as `top`, `left`, `margin`, and `padding`) all cause
|
||
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 (for example, a sidebar that pushes main content over), prefer [FLIP](https://aerotwist.com/blog/flip-your-animations/). FLIP allows you to change expensive
|
||
properties once, and handle the actual animation with transforms.
|
||
|
||
## Reducing Asset Footprint
|
||
|
||
### Universal code
|
||
|
||
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.
|
||
Where possible we should aim to remove modules from these bundles to reduce our
|
||
code footprint.
|
||
|
||
### Page-specific JavaScript
|
||
|
||
Webpack has been configured to automatically generate entry point bundles based
|
||
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 is compiled into a webpack
|
||
bundle and included on the page.
|
||
|
||
Previously, GitLab encouraged the use of
|
||
`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.
|
||
|
||
NOTE:
|
||
When unsure what controller and action corresponds to a page,
|
||
inspect `document.body.dataset.page` in your
|
||
browser's developer console from any page in GitLab.
|
||
|
||
#### Important Considerations
|
||
|
||
- **Keep Entry Points Lite:**
|
||
Page-specific JavaScript entry points should be as lite as possible. These
|
||
files are exempt from unit tests, and should be used primarily for
|
||
instantiation and dependency injection of classes and methods that live in
|
||
modules outside of the entry point script. Just import, read the DOM,
|
||
instantiate, and nothing else.
|
||
|
||
- **`DOMContentLoaded` should not be used:**
|
||
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`". Because the document is already
|
||
parsed, `DOMContentLoaded` is not needed to bootstrap applications because all
|
||
the DOM nodes are already at our disposal.
|
||
|
||
- **JavaScript that relies on CSS for calculations should use [`waitForCSSLoaded()`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/helpers/startup_css_helper.js#L34):**
|
||
GitLab uses [Startup.css](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38052)
|
||
to improve page performance. This can cause issues if JavaScript relies on CSS
|
||
for calculations. To fix this the JavaScript can be wrapped in the
|
||
[`waitForCSSLoaded()`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/helpers/startup_css_helper.js#L34)
|
||
helper function.
|
||
|
||
```javascript
|
||
import initMyWidget from './my_widget';
|
||
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
|
||
|
||
waitForCSSLoaded(initMyWidget);
|
||
```
|
||
|
||
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();
|
||
```
|
||
|
||
For example, see how we use this in [`app/assets/javascripts/pages/projects/graphs/charts/index.js`](https://gitlab.com/gitlab-org/gitlab/-/commit/5e90885d6afd4497002df55bf015b338efcfc3c5#02e81de37f5b1716a3ef3222fa7f7edf22c40969_9_8):
|
||
|
||
```javascript
|
||
waitForCSSLoaded(() => {
|
||
const languagesContainer = document.getElementById('js-languages-chart');
|
||
//...
|
||
});
|
||
```
|
||
|
||
- **Supporting Module Placement:**
|
||
- If a class or a module is _specific to a particular route_, try to locate
|
||
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 (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 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 (for example, `../shared/my_widget`).
|
||
|
||
- **Enterprise Edition Caveats:**
|
||
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 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
|
||
functionality.
|
||
|
||
### Code Splitting
|
||
|
||
Code that does not need to be run immediately upon page load (for example,
|
||
modals, dropdowns, and other behaviors that can be lazy-loaded) should be split
|
||
into asynchronous chunks with dynamic import statements. These
|
||
imports return a Promise which is resolved after the script has loaded:
|
||
|
||
```javascript
|
||
import(/* webpackChunkName: 'emoji' */ '~/emoji')
|
||
.then(/* do something */)
|
||
.catch(/* report error */)
|
||
```
|
||
|
||
Use `webpackChunkName` when generating dynamic imports as
|
||
it provides a deterministic filename for the chunk which can then be cached
|
||
in the browser across GitLab versions.
|
||
|
||
More information is available in [webpack's code splitting documentation](https://webpack.js.org/guides/code-splitting/#dynamic-imports).
|
||
|
||
### Minimizing page size
|
||
|
||
A smaller page size means the page loads faster, especially on mobile
|
||
and poor connections. The page is parsed more quickly by the browser, and less
|
||
data is used for users with capped data plans.
|
||
|
||
General tips:
|
||
|
||
- Don't add new fonts.
|
||
- 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.
|
||
- Use code-splitting dynamic imports wherever possible to lazy-load code that is not needed initially.
|
||
- [High Performance Animations](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/)
|
||
|
||
---
|
||
|
||
## Additional Resources
|
||
|
||
- [WebPage Test](https://www.webpagetest.org) for testing site loading time and size.
|
||
- [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) grades web pages and provides feedback to improve the page.
|
||
- [Profiling with Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/)
|
||
- [Browser Diet](https://browserdiet.com/) is a community-built guide that catalogues practical tips for improving web page performance.
|