Code splitting

Large apps degrade user experience and one way to control that is to split our code into chunks. This chapter will walk through tools afforded by Shadow to split our app code. We'll also implement the splits and analyze performance gains.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the Tinycanva: Clojure for React Developers course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Tinycanva: Clojure for React Developers, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Tinycanva: Clojure for React Developers
  • [00:00 - 00:11] The JS ecosystem makes developing JS apps fast, but installing libraries hurt the bundle size. It is normal for a JS app to be 1 to 5 megabytes before Gzip.

    [00:12 - 00:24] This means that the client has to parse at least one MB of JS code to make any sense. This might not be a problem for shiny MacBooks, but is definitely a concern for low-end mobile hardware.

    [00:25 - 00:36] Shadow provides us an efficient API to analyze and split our code into smaller chunks. Before we get into the details of code splitting, we need a way to measure where we stand.

    [00:37 - 00:45] Shadow comes with a built-in reporting tool that lists the size and ingredients of bundled modules. We can generate a report using YAN report command.

    [00:46 - 00:56] This report command was set up by create ShadowCLJ's app. After running this command, the output will be printed to your terminal and also in a file called report.html.

    [00:57 - 01:05] Our app is currently 625kb with any code splitting. The report also tells us about the elephants in their home.

    [01:06 - 01:15] The four most heavy modules consist of 50% of the code. Our project files, i.e. the code we wrote, are only 1.3% of the compiled code.

    [01:16 - 01:28] We will try and optimize this and regenerate reports to check our progress. The process of code splitting includes two steps, configuring the compiler and modifying the app to load split code.

    [01:29 - 01:39] With just one large bundle, we don't need to worry if our code is loaded yet. But with multiple smaller bundles, we need to ensure that all required code is available.

    [01:40 - 01:50] One way to split code is to bundle code by routes, i.e. have one module per route. We have three main routes that we can split for, login, register and graphics.

    [01:51 - 02:01] In the real world, you might also want to split out heavy components. For example, in our case, we can't really do anything about blueprint because that's a UI layer.

    [02:02 - 02:10] But Firebase and Fabric are not needed to render the index route. Shadow splits code by clubbing entry points into modules.

    [02:11 - 02:25] Modules is a generic term, but you can think of splitting out modules as files. Currently, our shadow configuration only generates one module main, i.e. only one file main.js that we load in index.html.

    [02:26 - 02:36] Let's update our configuration to generate one module per route. Module Loader True injects additional code that enables fetching of modules at runtime.

    [02:37 - 02:51] We added three additional modules, login, register and graphics. This will lead to the creation of login.js, register.js and graphics.js along with main.js in the configured output directory.

    [02:52 - 03:04] Only one module needs to be dependency free, i.e. the route module that's absolutely needed for app to work. Other modules can mark dependencies using the depends on configuration, which accepts a set.

    [03:05 - 03:11] It is possible to depend on more than one module. All modules need to depend on the route module.

    [03:12 - 03:24] The entry ski accepts a vector of namespaces that needs to be clubbed inside that module. Shadow computes the dependency graph and tries its best to put most code outside the route module.

    [03:25 - 03:34] Think of it trying to generate a small main file and pushing heavy components to other modules. However, this is not always accurate and requires tuning.

    [03:35 - 03:46] We have the modules now, but if you run the app as it is, Shadow will complain about not being able to split. This is because the modules we split out are not being loaded dynamically.

    [03:47 - 04:03] Since we split out modules by routes and our routes are the agent components, we need to create a component capable of lazy loading modules. For other code which is not in React lifecycle, shadow.load.load.load function can be used to load modules as needed.

    [04:04 - 04:16] Load returns a goob.async.deffered and can be used like a promise. The string extra is the name of the module that you will identify and could be main or login in our example.

    [04:17 - 04:29] We need to create a lazy component because there is no easy way to use a promise with React Router. Let's create a lazy component to load our React components dynamically.

    [04:30 - 04:40] We use the shadow lazy namespace and call load method on the loadable argument in component did mount. Loadable is the component we want to load.

    [04:41 - 04:47] The load function accepts a loadable and a callback. A callback updates the loaded state to true.

    [04:48 - 04:59] Then in the render method we check if the loaded state is true and render accordingly. Loadable is an atom like object that can be de-referenced to get the dynamically loaded module.

    [05:00 - 05:09] The dynamically loaded module in our case will be a reagent component so we can render it as vector loadable. This will make sense just in a moment.

    [05:10 - 05:23] Shadow is not able to split modules because app.core requires all pages essentially leading to a graph that cannot be split. We can safely remove the vectors that require login, register and graphics page .

    [05:24 - 05:32] Now we need to load these modules using the lazy component. For this we need to require the lazy component and the shadow lazy package.

    [05:33 - 05:45] Finally we can update the root router to render components lazily. We replace the actual components with lazy comp and pass the additional component to SL loadable.

    [05:46 - 06:03] Shadow lazy loadable is a macro that takes a namespace qualified definition, figures out the module it is present in and provides an API to load this module. SL loadable is a macro and not a function hence cannot be more inside the lazy comp definition.

    [06:04 - 06:24] This is passed as the loadable argument to lazy comp which when de-referenced using add the rate returns the component. Now if you load the app in logged in state and navigate from index to login page you will see a network call to load log in.js and probably a message that says lazy loading.

    [06:25 - 06:40] Using lazy comp directly didn't work for graphics route as react router redirect has some issues with switch and it prevents component from re-rendering. You can check the link to stack overflow and github issues in each after notes.

    [06:41 - 06:55] By adding an extra wrapper graphics container the component loads fine. If you check the report again you will notice that we have reduced the bundle size to 1.83 MB which was more than 2 MB earlier.

    [06:56 - 07:08] This was mainly because fabric was split out and moved to graphics module. Depending on your app you might be able to create more modules and save more bytes initially leading to a snappy UX.

    [07:09 - 07:20] In this chapter we understood Shadow's lazy loading mechanism. We then configured Shadow to split the app into smaller modules and updated the code with the ability to load modules at runtime.

    [07:21 - 07:28] [End of audio]