神刀安全网

Loading dependencies asynchronously in React components

In aprevious blog post, we introduced a React -based application called Monod , our Markdown editor. During our Le lab session, we spent half a day optimizing loading time of Monod. We do use HTTP cache as soon as possible, but the very first request triggered by a client is usually not cached. Given how saving a few milliseconds is valuable, we worked on reducing Monod’s initial loading time in order to improve user experience. This blog post sums up our work on improving initial loading time of React components .

Loading dependencies asynchronously in React components

Ain’t React Fast Enough?

We are not tackling an issue caused by React here, but rather a performance issue implied by the combination of various third-party libraries used in our React components. Monod is a web-based Markdown editor, hence we need a couple of libraries for both editing and rendering content. Among all these libraries, we rely on CodeMirror and highlight.js . Both support many languages and provide different add-ons ( e.g. , modes in CodeMirror), which also implies a lot of code to load:

import Codemirror from 'react-codemirror';  import 'codemirror/mode/gfm/gfm'; import 'codemirror/mode/css/css'; // ... repeat the line above with all languages you want to support // CodeMirror supports 100+ languages /o/  export default class Markdown extends Component {   // ... }; 

Root Cause Analysis

We did not focus on these two libraries by chance or because we thought those were problematic, we tried to understand why initial loading time was so high by taking a look at some key numbers. For instance, webpack ‘s default output is rather interesting:

                         Asset       Size  Chunks             Chunk Names    app.d1439bcb7238e30bb09e.js     192 kB       0  [emitted]  app vendor.f9e3b7ef70fc64b82221.js     897 kB       1  [emitted]  vendor 

Our application code is split into two chunks: app and vendor , both fetched when the application is loading because they are required to render the interface. This means a browser must download 1MB of JavaScript (the figure below shows gzipped content) before being able to render anything to the user: no way!

Loading dependencies asynchronously in React components

One of the great (hidden) features of webpack is its profiling ability thanks to the --profile option along with the analyse interface . If you want something a bit more "visual", check out @batemanchris ‘s webpack visualizer . Thanks to those tools, we identified that both CodeMirror and highlight.js represented ~20% of the overall size each. Now, question is: how to speed up things?

Asynchronous Loading To The Rescue

Because we cannot improve the current build since it is already "uglified" and gzipped, we need another way to speed up the initial loading time. One simple yet efficient solution to this problem is to load code not when the application starts, but rather when the latter actually needs it. Fortunately, Jérôme Schneider blogged about an interesting solution a while ago .

The general idea is to leverage webpack ‘s require.ensure feature ( documented here ). It allows to create anonymous chunks that are automatically downloaded by webpack when needed. Jérôme introduced the notion of a loader per component to group dependencies into unique chunks. In other words, it creates extra JavaScript files that are loaded on demand.

const { Promise } = global;  export default () => {   return new Promise(resolve => {     require.ensure([], () => {       require('codemirror/mode/gfm/gfm');       require('codemirror/mode/css/css');       // ... repeat the line above with all languages you want to support       // CodeMirror supports 100+ languages /o/        resolve();     });   }); }; 

Additionally, we got rid of the vendor chunk, that is why the app ‘s size is bigger than before. Yet, this is now the only JavaScript file required for the application to load. The unnamed chunk ( d77feac34fd6da476550.js ) is related to the loader given above.

                         Asset       Size  Chunks             Chunk Names    app.ec1540506ca6a0bd44c3.js     341 kB       0  [emitted]  app        d07c55292dcbdb3e91c3.js     144 kB       1  [emitted] 

The new version of our Markdown component is given below. When the component is about to be mounted, the loader is triggered, and once its dependencies are all required, we force to render the component again, which, in this case, loads the syntax highlighting of the "raw" Markdown content.

import Codemirror from 'react-codemirror'; import MarkdownLoader from './loaders/Markdown';  export default class Markdown extends Component {   // ...    componentWillMount() {     MarkdownLoader().then(() => {       this.forceUpdate();     });   } }; 

We chose to load the dependencies as soon as possible, hence the use of componentWillMount() , which seems to be a decent choice . This works pretty well for asynchronously loading dependencies that work out of the box (such as libraries loading or connecting to a global object for instance). Nonetheless, a common use case is to inject dependencies into the component for invoking their methods, e.g. , a Markdown compiler or a syntax highlighter.

Let’s take an example with the Preview component used to render the "raw" content written in Markdown (in the Markdown component) into pretty HTML. The Preview component uses marked and highlight.js to render the HTML content. The way the component below is defined allows to directly render the content when the application is loaded, yet it requires to load all the dependencies a priori , which is not what we want.

import marked from 'marked'; import hljs from 'highlight.js';  export default class Preview extends Component {   // ...    componentWillMount() {     this.marked = marked.setOptions({       highlight: (code) => hljs.highlightAuto(code).value     });   }    getHTML() {     return {       _html: this.marked(this.props.raw.toString());     };   }    render() {     return (       <div dangerouslySetInnerHTML={this.getHTML()} />     );   } }; 

We leveraged the notion of loader and came up with a PreviewLoader that roughly looked like the one below. It loads the dependencies just like the first loader except that it passes them as the result of the promise, so that the React component can manipulate them.

const { Promise } = global;  export default () => {   return new Promise(resolve => {     require.ensure([], () => {       resolve({         hljs: require('highlight.js'),         marked: require('marked')       });     });   }); }; 

We pass this loader as a component’s property , and use the dependencies as before except that we have to check whether the dependencies are loaded or not. That is why we have a conditional statement in the getHTML() method. In our example below, when this.marked is undefined , we return a temporary message. On the other hand, you may wonder why we use a property here, and we will get back to that in a minute (hint: "testing").

import PreviewLoader from './loaders/Preview';  const { func } = PropTypes;  export default class Preview extends Component {   // ...    componentWillMount() {     this.props.previewLoader().then((deps) => {       this.marked = deps.marked.setOptions({         highlight: (code) => deps.hljs.highlightAuto(code).value       });        this.forceUpdate();     });   }    getHTML() {     return {       _html: this.marked ? this.marked(this.props.raw.toString()) : 'Loading...';     };   } };  Preview.propTypes = {   previewLoader: func.isRequired };  Preview.defaultProps = {   previewLoader: PreviewLoader }; 

The main difference between the two versions of this component is that we now load its dependencies in an asynchronous manner: achievement unlocked! By doing this, we have reduced the loading time by a factor of 2 (~600 milliseconds). We have reproduced this experiment several times, and got similar results, but these numbers are not particularly interesting. Indeed, what has really been improved is the user’s perception.

Loading dependencies asynchronously in React components

Testing

What we have done so far works well on our machine™, but our test suite has to be refactored to deal with such changes. Testing asynchronous behaviors is not particularly easy, especially when code is too coupled. That is exactly why we used a React’s property to pass our loader to the component. It allows to inject either mocks or fakes , which becomes useful to avoid relying on webpack’s require.ensure feature.

In the example below, we decided to test the complete behavior of the Preview component, i.e. the whole process of converting "raw" content into HTML. This implies the execution of the component’s dependencies. Our testing environment is not tied to webpack , so we have to fake the PreviewLoader instance. This is done in mocha ‘s before statement:

import React from 'react'; import { mount, shallow, render } from 'enzyme'; import { expect } from 'chai';  import marked from 'marked'; import hljs from 'highlight.js';  // see: https://github.com/mochajs/mocha/issues/1847 const { before, describe, it, Promise } = global;  import Preview from '../Preview';  describe('<Preview />', () => {   let previewLoader;    before(() => {     previewLoader = () => {       return Promise.resolve({         marked: marked,         hljs: hljs       });     };   });    it('converts markdown into HTML', (done) => {     const wrapper = mount(       <Preview raw={"*italic*"} previewLoader={previewLoader} />     );      setTimeout(() => {       expect(wrapper.html()).to.contain('<em>italic</em>');        done();     }, 5);   }); }); 

The use of setTimeout() to ensure that the dependencies are all wired into the Preview component before asserting it is a workaround though, but it is mandatory to not get the Loading... temporary message. This is not the best thing we have done, but it works. If you have a better suggestion, please ping us !

Overall, we significantly improved user experience by reducing the initial load time. Another implied benefit is the ability to add many new features , such as supporting more languages in Monod ‘s case. Nonetheless, the setTimeout() trick is still an issue we would like not to see in our test files. Do not hesitate to ping us on Twitter for anything related to this article!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Loading dependencies asynchronously in React components

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮