debian-mirror-gitlab/doc/development/fe_guide/performance.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

478 lines
20 KiB
Markdown
Raw Normal View History

2021-01-29 00:20:46 +05:30
---
stage: none
group: unassigned
2022-11-25 23:54:43 +05:30
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
2021-01-29 00:20:46 +05:30
---
2017-08-17 22:00:37 +05:30
# Performance
2021-02-22 17:27:13 +05:30
Performance is an essential part and one of the main areas of concern for any modern application.
2022-08-27 11:52:29 +05:30
## Monitoring
We have a performance dashboard available in one of our [Grafana instances](https://dashboards.gitlab.net/d/000000043/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://www.sitespeed.io/) every 4 hours. These changes are displayed after a set number of pages are aggregated.
These pages can be found inside text files in the [`sitespeed-measurement-setup` repository](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup) called [`gitlab`](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup/-/tree/master/gitlab)
Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing URLs of pages to the text files. The changes are pushed live on the next scheduled run after the changes are merged into `main`.
There are 3 recommended high impact metrics (core web vitals) to review on each page:
- [Largest Contentful Paint](https://web.dev/lcp/)
- [First Input Delay](https://web.dev/fid/)
- [Cumulative Layout Shift](https://web.dev/cls/)
For these metrics, lower numbers are better as it means that the website is more performant.
2021-02-22 17:27:13 +05:30
## 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
2021-04-29 21:17:54 +05:30
It takes several arguments of which the measurement's name is the only one required. Examples:
2021-02-22 17:27:13 +05:30
- Duration between the start and end marks:
```javascript
performance.measure('My component', 'my-component-start', 'my-component-end')
```
2021-03-08 18:12:59 +05:30
- Duration between a mark and the moment the measurement is taken. The end mark is omitted in
2021-02-22 17:27:13 +05:30
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');
```
2022-08-27 11:52:29 +05:30
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.
2021-02-22 17:27:13 +05:30
### 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:
2023-05-27 22:25:52 +05:30
```javascript
import PerformancePlugin from '~/performance/vue_performance_plugin';
```
2021-02-22 17:27:13 +05:30
1. Use it before initializing your Vue application:
2023-05-27 22:25:52 +05:30
```javascript
Vue.use(PerformancePlugin, {
components: [
'IdeTreeList',
'FileTree',
'RepoEditor',
]
});
```
2021-02-22 17:27:13 +05:30
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
2021-04-29 21:17:54 +05:30
`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.
2021-02-22 17:27:13 +05:30
NOTE:
This pattern is a recommendation and not a hard rule.
```javascript
2021-04-29 21:17:54 +05:30
app-*-start // for a start 'mark'
app-*-end // for an end 'mark'
app-* // for 'measure'
2021-02-22 17:27:13 +05:30
```
2021-03-08 18:12:59 +05:30
For example, `'webide-init-editor-start`, `mr-diffs-mark-file-tree-end`, and so on. We do it to
2021-02-22 17:27:13 +05:30
help identify marks and measures coming from the different apps on the same page.
2017-08-17 22:00:37 +05:30
## Best Practices
2021-03-11 19:13:27 +05:30
### Real-time Components
2017-08-17 22:00:37 +05:30
2021-03-11 19:13:27 +05:30
When writing code for real-time features we have to keep a couple of things in mind:
2019-07-07 11:18:12 +05:30
2017-08-17 22:00:37 +05:30
1. Do not overload the server with requests.
2021-03-11 19:13:27 +05:30
1. It should feel real-time.
2017-08-17 22:00:37 +05:30
2021-03-11 19:13:27 +05:30
Thus, we must strike a balance between sending requests and the feeling of real-time.
Use the following rules when creating real-time solutions.
2017-08-17 22:00:37 +05:30
2021-10-27 15:23:28 +05:30
<!-- vale gitlab.Spelling = NO -->
2021-02-22 17:27:13 +05:30
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).
2019-07-07 11:18:12 +05:30
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
2018-11-18 11:00:15 +05:30
1. A response with HTTP status different from 2XX should disable polling as well.
2017-08-17 22:00:37 +05:30
1. Use a common library for polling.
2022-08-27 11:52:29 +05:30
1. Poll on active tabs only. Use [Visibility](https://github.com/ai/visibilityjs).
2021-10-27 15:23:28 +05:30
1. Use regular polling intervals, do not use backoff polling or jitter, as the interval is
2019-07-07 11:18:12 +05:30
controlled by the server.
2021-03-11 19:13:27 +05:30
1. The backend code is likely to be using ETags. You do not and should not check for status
2021-02-22 17:27:13 +05:30
`304 Not Modified`. The browser transforms it for you.
2017-08-17 22:00:37 +05:30
2021-10-27 15:23:28 +05:30
<!-- vale gitlab.Spelling = YES -->
2018-05-09 12:01:36 +05:30
### Lazy Loading Images
2017-09-10 17:25:29 +05:30
2018-11-18 11:00:15 +05:30
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,
2021-02-22 17:27:13 +05:30
the value of `data-src` is moved to `src` automatically if the image is in the current viewport.
2017-09-10 17:25:29 +05:30
2021-03-11 19:13:27 +05:30
- Prepare images in HTML for lazy loading by renaming the `src` attribute to `data-src` and adding the class `lazy`.
2021-02-22 17:27:13 +05:30
- If you are using the Rails `image_tag` helper, all images are lazy-loaded by default unless `lazy: false` is provided.
2017-09-10 17:25:29 +05:30
2021-03-11 19:13:27 +05:30
When asynchronously adding content which contains lazy images, call the function
2021-02-22 17:27:13 +05:30
`gl.lazyLoader.searchLazyImages()` which searches for lazy images and loads them if needed.
2021-03-11 19:13:27 +05:30
In general, it should be handled automatically through a `MutationObserver` in the lazy loading function.
2017-09-10 17:25:29 +05:30
2018-03-27 19:54:05 +05:30
### 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
2020-05-24 23:13:21 +05:30
[High Performance Animations](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/).
2018-03-27 19:54:05 +05:30
2021-03-11 19:13:27 +05:30
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
2018-03-27 19:54:05 +05:30
properties once, and handle the actual animation with transforms.
2021-06-08 01:23:25 +05:30
### Prefetching assets
In addition to prefetching data from the [API](graphql.md#making-initial-queries-early-with-graphql-startup-calls)
we allow prefetching the named JavaScript "chunks" as
[defined in the Webpack configuration](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/webpack.config.js#L298-359).
We support two types of prefetching for the chunks:
- The [`prefetch` link type](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/prefetch)
is used to prefetch a chunk for the future navigation
2021-09-30 23:02:18 +05:30
- The [`preload` link type](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload)
is used to prefetch a chunk that is crucial for the current navigation but is not
2021-06-08 01:23:25 +05:30
discovered until later in the rendering process
2021-09-30 23:02:18 +05:30
Both `prefetch` and `preload` links bring the loading performance benefit to the pages. Both are
2021-06-08 01:23:25 +05:30
fetched asynchronously, but contrary to [deferring the loading](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer)
of the assets which is used for other JavaScript resources in the product by default, `prefetch` and
2021-09-30 23:02:18 +05:30
`preload` neither parse nor execute the fetched script unless explicitly imported in any JavaScript
module. This allows to cache the fetched resources without blocking the execution of the
2021-06-08 01:23:25 +05:30
remaining page resources.
2021-09-30 23:02:18 +05:30
To prefetch a JavaScript chunk in a HAML view, `:prefetch_asset_tags` with the combination of
2021-06-08 01:23:25 +05:30
the `webpack_preload_asset_tag` helper is provided:
```javascript
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
```
This snippet will add a new `<link rel="preload">` element into the resulting HTML page:
```HTML
<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">
```
2021-09-30 23:02:18 +05:30
By default, `webpack_preload_asset_tag` will `preload` the chunk. You don't need to worry about
`as` and `type` attributes for preloading the JavaScript chunks. However, when a chunk is not
2021-06-08 01:23:25 +05:30
critical, for the current navigation, one has to explicitly request `prefetch`:
```javascript
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
```
This snippet will add a new `<link rel="prefetch">` element into the resulting HTML page:
```HTML
<link rel="prefetch" href="/assets/webpack/monaco.chunk.js">
```
2017-08-17 22:00:37 +05:30
## Reducing Asset Footprint
2018-05-09 12:01:36 +05:30
### Universal code
2017-08-17 22:00:37 +05:30
2021-02-22 17:27:13 +05:30
Code that is contained in `main.js` and `commons/index.js` is loaded and
2021-03-11 19:13:27 +05:30
run on _all_ pages. **Do not add** anything to these files unless it is truly
2018-05-09 12:01:36 +05:30
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
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
Webpack has been configured to automatically generate entry point bundles based
2021-02-22 17:27:13 +05:30
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
2018-05-09 12:01:36 +05:30
pages.
2020-06-23 00:09:42 +05:30
For example, if you were to visit <https://gitlab.com/gitlab-org/gitlab/-/issues>,
2018-05-09 12:01:36 +05:30
you would be accessing the `app/controllers/projects/issues_controller.rb`
controller with the `index` action. If a corresponding file exists at
2021-02-22 17:27:13 +05:30
`pages/projects/issues/index/index.js`, it is compiled into a webpack
2018-05-09 12:01:36 +05:30
bundle and included on the page.
2021-01-29 00:20:46 +05:30
Previously, GitLab encouraged the use of
2021-02-22 17:27:13 +05:30
`content_for :page_specific_javascripts` in HAML files, along with
2019-12-21 20:55:43 +05:30
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.
2021-02-22 17:27:13 +05:30
NOTE:
2021-03-11 19:13:27 +05:30
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.
2018-05-09 12:01:36 +05:30
2019-12-04 20:38:33 +05:30
#### Important Considerations
2018-05-09 12:01:36 +05:30
- **Keep Entry Points Lite:**
2020-03-13 15:44:24 +05:30
Page-specific JavaScript entry points should be as lite as possible. These
2018-05-09 12:01:36 +05:30
files are exempt from unit tests, and should be used primarily for
instantiation and dependency injection of classes and methods that live in
2020-03-13 15:44:24 +05:30
modules outside of the entry point script. Just import, read the DOM,
2018-05-09 12:01:36 +05:30
instantiate, and nothing else.
2021-01-29 00:20:46 +05:30
- **`DOMContentLoaded` should not be used:**
2021-02-22 17:27:13 +05:30
All GitLab JavaScript files are added with the `defer` attribute.
2021-01-29 00:20:46 +05:30
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
2021-03-11 19:13:27 +05:30
been parsed, but before firing `DOMContentLoaded`". Because the document is already
2021-01-29 00:20:46 +05:30
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.
2018-05-09 12:01:36 +05:30
2019-10-12 21:52:04 +05:30
```javascript
import initMyWidget from './my_widget';
2021-01-29 00:20:46 +05:30
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
waitForCSSLoaded(initMyWidget);
```
2018-11-18 11:00:15 +05:30
2021-01-29 00:20:46 +05:30
Note that `waitForCSSLoaded()` methods supports receiving the action in different ways:
2021-02-22 17:27:13 +05:30
2021-01-29 00:20:46 +05:30
- With a callback:
2021-02-22 17:27:13 +05:30
2021-01-29 00:20:46 +05:30
```javascript
waitForCSSLoaded(action)
```
2021-02-22 17:27:13 +05:30
2021-01-29 00:20:46 +05:30
- With `then()`:
2021-02-22 17:27:13 +05:30
2021-01-29 00:20:46 +05:30
```javascript
waitForCSSLoaded().then(action);
```
2021-02-22 17:27:13 +05:30
2021-01-29 00:20:46 +05:30
- With `await` followed by `action`:
2021-02-22 17:27:13 +05:30
2021-01-29 00:20:46 +05:30
```javascript
await waitForCSSLoaded;
action();
```
2021-03-11 19:13:27 +05:30
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):
2021-01-29 00:20:46 +05:30
```javascript
waitForCSSLoaded(() => {
const languagesContainer = document.getElementById('js-languages-chart');
//...
2019-10-12 21:52:04 +05:30
});
```
2018-05-09 12:01:36 +05:30
2018-11-18 11:00:15 +05:30
- **Supporting Module Placement:**
2019-10-12 21:52:04 +05:30
- If a class or a module is _specific to a particular route_, try to locate
2021-02-22 17:27:13 +05:30
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
2019-10-12 21:52:04 +05:30
should place the module at `pages/widget/show/my_widget.js` and import it
2021-02-22 17:27:13 +05:30
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
2019-10-12 21:52:04 +05:30
shared directory at the closest common parent directory for the entry
2021-02-22 17:27:13 +05:30
points that import it. For example, if `my_widget.js` is imported in
2019-10-12 21:52:04 +05:30
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
2021-02-22 17:27:13 +05:30
a relative path if possible (for example, `../shared/my_widget`).
2018-05-09 12:01:36 +05:30
- **Enterprise Edition Caveats:**
2021-02-22 17:27:13 +05:30
For GitLab Enterprise Edition, page-specific entry points override their
2018-05-09 12:01:36 +05:30
Community Edition counterparts with the same name, so if
2021-02-22 17:27:13 +05:30
`ee/app/assets/javascripts/pages/foo/bar/index.js` exists, it takes
2020-03-13 15:44:24 +05:30
precedence over `app/assets/javascripts/pages/foo/bar/index.js`. If you want
2018-05-09 12:01:36 +05:30
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.
2017-08-17 22:00:37 +05:30
### Code Splitting
2021-03-11 19:13:27 +05:30
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
2021-02-22 17:27:13 +05:30
imports return a Promise which is resolved after the script has loaded:
2018-05-09 12:01:36 +05:30
```javascript
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(/* do something */)
.catch(/* report error */)
```
2021-03-11 19:13:27 +05:30
Use `webpackChunkName` when generating dynamic imports as
2021-02-22 17:27:13 +05:30
it provides a deterministic filename for the chunk which can then be cached
2021-03-11 19:13:27 +05:30
in the browser across GitLab versions.
2018-05-09 12:01:36 +05:30
2022-08-27 11:52:29 +05:30
More information is available in [webpack's code splitting documentation](https://webpack.js.org/guides/code-splitting/#dynamic-imports) and [vue's dynamic component documentation](https://v2.vuejs.org/v2/guide/components-dynamic-async.html).
2017-08-17 22:00:37 +05:30
### Minimizing page size
2021-03-11 19:13:27 +05:30
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
2017-08-17 22:00:37 +05:30
data is used for users with capped data plans.
General tips:
- Don't add new fonts.
2021-02-22 17:27:13 +05:30
- Prefer font formats with better compression, for example, WOFF2 is better than WOFF, which is better than TTF.
2017-08-17 22:00:37 +05:30
- 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.
2018-05-09 12:01:36 +05:30
- 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.
2020-05-24 23:13:21 +05:30
- [High Performance Animations](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/)
2017-08-17 22:00:37 +05:30
2019-10-12 21:52:04 +05:30
---
2017-08-17 22:00:37 +05:30
## Additional Resources
2019-12-21 20:55:43 +05:30
- [WebPage Test](https://www.webpagetest.org) for testing site loading time and size.
2022-06-21 17:19:12 +05:30
- [Google PageSpeed Insights](https://pagespeed.web.dev/) grades web pages and provides feedback to improve the page.
2021-09-30 23:02:18 +05:30
- [Profiling with Chrome DevTools](https://developer.chrome.com/docs/devtools/)
2020-05-24 23:13:21 +05:30
- [Browser Diet](https://browserdiet.com/) is a community-built guide that catalogues practical tips for improving web page performance.