As of October 1, 2023, LINE has been rebranded as LY Corporation. Visit the new blog of LY Corporation here: LY Corporation Tech Blog

Blog


Easy code-splitting with grow-loader

Hi, I'm @sunderls from LINE MANGA team where I mostly write JavaScript. Did you know that you can read free Manga directly from LINE1)? Has anyone tried it? As we shared on our previous post, LINE MANGA: Smooth page transition with Page Stack, LINE MANGA is built with web technologies. Because it runs in within the LINE app, we have put in a great effort to create a smooth user experience, and as one of the means to accomplish our goal, we have chosen code-splitting. Today, I'd like to introduce the grow-loader, LINE's open source project for code-splitting.

Why split code?

Our JavaScript source code is built as a single bundle. As our service grows, more pages are created and more complicated features are implemented, and the single bundle becomes larger and larger. Having some serious thoughts about the future, we decided to bring in code-splitting before it was too late.

Why not use common HOC solutions?

At first, we tried to use common HOC (Higher Order Component) solutions such as react-loadable, but we faced critical issues as follows:

  • Page transition was not instant anymore

    The reason is obvious. HOC solutions delay loading components after a user interaction. Page will transit after components are loaded, so page transition is no longer instant. For LINE MANGA, this is a serious issue.

  • Preloading helps, but not for long pages

    We can have all of the "next" pages preloaded before user interaction, and this can be helpful in some cases. But there are a lot, I mean, a lot of pages in LINE MANGA, it becomes difficult to manage the preloading. And personally, preloading components that may not be accessed at all is against the goal of code-splitting.

    Having many long pages in our app, preloading doesn't help much anyway, because the time taken to render the DOM is not affected by preloading.

  • We can add a common loading indicator, but we are not going to

    We don't use a common loading indicator, because we already have placeholder components for almost every page. Rather than implementing placeholders separately, we have directly included placeholder components in the pages with dummy data. So, splitting placeholder components to separate files is difficult.

  • HOC solutions didn't work well with our customized hooks

    To achieve instant page transition, we organized pages in a Page Stack, and added customized component lifecycle hooks like componentDidHide. If we are to integrate HOC solutions for code-splitting, we had to modify too much of our code. We tried and we gave up.

As I have mentioned, smoothness is something we do not compromise!

What we did

After giving up HOC Solutions, we thought more about our problems as the following:

  • If we keep placeholder components in the entry bundle, we can keep the smoothness.
  • If we keep placeholder components as they are and split all other parts to another file, then long pages will be rendered faster because of a two-step rendering.
  • Maybe we can create a webpack loader to split the code automatically.

This is how grow-loader was created.

What is grow-loader?

With grow-loader, our own webpack loader, we can make methods dynamically imported just by adding the decorator, @grow. Here is an example. See how @grow is added for the methodToGrow() and methodToGrowAndBind() methods.

class SampleClass {

    @grow
    methodToGrow() {
        // ...
    }

    @grow
    methodToGrowAndBind = () => {
        // ...
    }

    methodToBeBundled(){
        // ...
    }
}

With grow-loader working, the SampleClass will:

  • Have no methodToGrow() and no methodToGrowAndBind() methods by default.
  • Have a new method grow(), which will load the two methods back into the class.

Here is an example of using grow-loader.

const sample = new SampleClass();
console.assert(a.methodToGrow === undefined);
console.assert(a.methodToGrowAndBind === undefined);

sample.grow().then(() => {
    sample.methodToGrow();
    sample.methodToGrowAndBind();
});

Using grow-loader with React

Suppose we have a class, LongPage, which has methods we'd like to split out.

export default class LongPage extends React.Component {

    methodToGrow() {
        // ..
    }

    methodToGrowAndBind = () => {
        // ..
    }

    methodToBeBundled(){
        // ...
    }

    render(){
        return <div>
            this is a very long page
        </div>
    }
}

First we create a common component for all pages of the app.

class GrowablePage extends React.Component {
    // Load split methods at componentDidMount
    componentDidMount() {
        if (this.grow) {
            this.grow().then(() => {
                this.hasGrown = true;
                this.forceUpdate();
            });
        }
    }
}

Then we break up the original render() method, which renders quite a long and complex page, into two methods; the render() method and the renderMore() method. The render() method is now much simpler and slimmer. We make the renderMore() method render the parts of the page that are not required to be bundled, and add the @grow decorator to the renderMore() method.

export default class LongPage extends GrowablePage {

    @grow
    methodToGrow() {
        // ...
    }

    @grow
    methodToGrowAndBind = () => {
        // ...
    }

    methodToBeBundled(){
        // ...
    }

    @grow
    renderMore() {
        return <div>
            this is below the first view
        </div>
    }

    render(){
        return <div>
            this is basic part
            { this.hasGrown ? this.renderMore() : null}
        </div>
    }
}

Result of using grow-loader

The grow-loader is just a tool to transform code, so how much of the file size is cut down depends on how it is used. As you can see in the previous example, the more code we move from the render() method to the renderMore() method and the more @grow decorators we add, the size of a built bundle will become smaller.

In LINE MANGA, we cut down about 15% of the bundle size with grow-loader. In terms of cutting down loading time, we cannot compare our result directly with that of common HOC solutions, because HOC solutions delay loading the whole component. However, we have achieved a 40% decrease in mount time of our top page, that is, the time between calling the constructor() method and the componentDidMount() method. This is why we think our implementation was acceptable.

The grow-loader is now public on Github. Go and have a try. Bug reports or ideas are welcomed!

1) LINE MANGA is only available in Japan.