diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md index 98e43931a02..82e110ba8e3 100644 --- a/doc/development/fe_guide/performance.md +++ b/doc/development/fe_guide/performance.md @@ -49,39 +49,95 @@ properties once, and handle the actual animation with transforms. ### Page-specific JavaScript -Certain pages may require the use of a third party library, such as [d3][d3] for -the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These -libraries increase the page size significantly, and impact load times due to -bandwidth bottlenecks and the browser needing to parse more 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 are meant to correspond to Rails controllers and +actions. These auto-generated bundles will be automatically included on the +corresponding pages. -In cases where libraries are only used on a few specific pages, we use -"page-specific JavaScript" to prevent the main `main.js` file from -becoming unnecessarily large. +For example, if you were to visit [gitlab.com/gitlab-org/gitlab-ce/issues](https://gitlab.com/gitlab-org/gitlab-ce/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 +bundle and included on the page. -Steps to split page-specific JavaScript from the main `main.js`: +> **Note:** Previously we had encouraged the use of +> `content_for :page_specific_javascripts` within 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. -1. Create a directory for the specific page(s), e.g. `graphs/`. -1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`. -1. Add the new "bundle" file to the list of entry files in `config/webpack.config.js`. - - For example: `graphs: './graphs/graphs_bundle.js',`. -1. Move code reliant on these libraries into the `graphs` directory. -1. In `graphs_bundle.js` add CommonJS `require('./path_to_some_component.js');` statements to load any other files in this directory. Make sure to use relative urls. -1. In the relevant views, add the scripts to the page with the following: +In addition to these page-specific bundles, the code within `main.js` and +`commons/index.js` are imported on _all_ pages. -```haml -- content_for :page_specific_javascripts do - = webpack_bundle_tag 'lib_chart' - = webpack_bundle_tag 'graphs' -``` +#### Important Tips and Considerations: -The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js` -is separated from the bundle file so it can be cached separately from the bundle -and reused for other pages that also rely on the library. For an example, see -[this Haml file][page-specific-js-example]. +- If you are unsure what controller and action corresponds to a given page, you + can find this out by checking `document.body.dataset.page` while on any page + within gitlab.com. + +- Since `main.js` and `commons/index.js` are imported on all pages, it is + important to not add anything to these bundles unless it is truly needed + _everywhere_. This includes ubiquitous libraries like `vue`, `axios`, and + `jQuery`, as well as code for the main navigation and sidebar. + +- Page-specific javascript entry points should be as lite as possible. These + files are exempt from 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. + +- **DO NOT ASSUME** that the DOM has been fully loaded and available when an + entry point script is run. If you require that some code be run after the + DOM has loaded, you must attach an event handler to the `DOMContentLoaded` + event with: + + ```javascript + import initMyWidget from './my_widget'; + + document.addEventListener('DOMContentLoaded', () => { + initMyWidget(); + }); + ``` + +- 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 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 shared + directory at the closest common parent directory for the entry points that + import it. For example, if `my_widget.js` is imported within 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 `../shared/my_widget`. + +- For GitLab Enterprise Edition, page-specific entry points will override their + Community Edition counterparts with the same name, so if + `ee/app/assets/javascripts/pages/foo/bar/index.js` exists, it will take + 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 -> *TODO* flesh out this section once webpack is ready for code-splitting +For any code that does not need to be run immediately upon page load, (e.g. +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: + +```javascript +import(/* webpackChunkName: 'emoji' */ '~/emoji') + .then(/* do something */) + .catch(/* report error */) +``` + +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 +the browser. + +More information is available in [webpack's code splitting documentation](https://webpack.js.org/guides/code-splitting/#dynamic-imports). ### Minimizing page size @@ -95,7 +151,8 @@ General tips: - Prefer font formats with better compression, e.g. 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 dynamically load libraries that are only needed on certain pages. +- 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][high-perf-animations] ------- @@ -112,8 +169,5 @@ General tips: [pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ [google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en [browser-diet]: https://browserdiet.com/ -[d3]: https://d3js.org/ -[chartjs]: http://www.chartjs.org/ -[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 [high-perf-animations]: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/ [flip]: https://aerotwist.com/blog/flip-your-animations/