Loading ES modules into browser applications

The other day they reproached me for saying that I didn’t know,”that it is still impossible to assemble a bundle from esm without transpilation“. Well, what can I say… I really don't know 🙂 In my opinion, es-modules were invented precisely to load JS-code directly into the browser as needed, and collecting modules into bundles is, well… like rubbing a cat the wrong way.

I understand that traditions / habits / business requirements / backward compatibility / corporate ethics, etc. They say that code for browser applications should be delivered in bundles, period! However, in some cases (small applications, rapid prototyping, distributed development), assembling bundles is unnecessary and the code can and should be loaded directly into the browser in the form of es-modules.

Static import

As an example I will give application codeconsisting of a single file that loads all the necessary modules through unpkg.com (global CDN for npm modules). You can package all your code as a set of npm packages, and keep only one head file, index.html, with your hoster. The demo application is quite useless from a practical point of view – I took the first two unrelated npm packages that came across, which are written in the form of es-modules:

  • store-esm: storing data in the browser in the form of “key/value” (the first versions of the original package were released in 2010, now the package is not popular);

  • @cloudfour/twing-browser-esm: usage Twig-templates in the browser.

The main thing about them is that these are not packages specially created by me for demonstration, but really the first ones I came across. First package (store-esm) consists of separate es-modules, the second package (@cloudfour/twing-browser-esm) – the most complete bundle in the form of one single es-module “weighing” 1.7 MB, purely for comparing the behavior of modules in the browser.

Code of the entire HTML file
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>The static import</title>
    <script type="module">
        // IMPORT
        import engine from 'https://unpkg.com/store-esm@3.0.0/src/store-engine.js';
        import storages from 'https://unpkg.com/store-esm@3.0.0/storages/all.js';
        import plugins from 'https://unpkg.com/store-esm@3.0.0/plugins/all.js';
        import {
            TwingEnvironment,
            TwingLoaderArray
        } from 'https://unpkg.com/@cloudfour/twing-browser-esm@5.1.1/dist/index.mjs';

        // FUNCS
        function useStore(engine, storages, plugins) {
            const store = engine.createStore(storages, plugins);
            store.set('user', {name: 'Alex Gusev'});
            console.log(store.get('user'));
        }

        function useTwing(TwingEnvironment, TwingLoaderArray) {
            const templates = {
                'index.twig': `
<h1>{{ title }}</h1>
<p>{{ message }}</p>
`
            };
            const loader = new TwingLoaderArray(templates);
            const twing = new TwingEnvironment(loader);
            const context = {
                title: 'Hello, guys!',
                message: 'Welcome to using Twing in the browser.'
            };
            twing.render('index.twig', context).then((output) => {
                document.body.innerHTML = output;
            }).catch((err) => {
                console.error('Error rendering template:', err);
            });
        }

        // MAIN
        useStore(engine, storages, plugins);
        useTwing(TwingEnvironment, TwingLoaderArray);
    </script>
</head>
<body></body>
</html>

Code for loading ES modules and using them:

<script type="module">
    // IMPORT
    import engine from 'https://unpkg.com/store-esm@3.0.0/src/store-engine.js';
    import storages from 'https://unpkg.com/store-esm@3.0.0/storages/all.js';
    import plugins from 'https://unpkg.com/store-esm@3.0.0/plugins/all.js';
    import {
        TwingEnvironment,
        TwingLoaderArray
    } from 'https://unpkg.com/@cloudfour/twing-browser-esm@5.1.1/dist/index.mjs';

    // FUNCS
    function useStore(engine, storages, plugins) {...}

    function useTwing(TwingEnvironment, TwingLoaderArray) {...}

    // MAIN
    useStore(engine, storages, plugins);
    useTwing(TwingEnvironment, TwingLoaderArray);
</script>

Main advantages

Using a CDN unpkg.com significantly reduces the requirements for your own hoster, and if the hoster “takes money” for the volume of downloaded information, then here the obvious financial benefit is already visible.

Optimization of Internet usage due to the possibility of caching GET requests to a single source at different levels. This is an advantage from the field of “Protecting the environment by sorting waste in your own kitchen“, and yet, it works on large volumes.

The ability to use different versions of the same package in different parts of the application. When I integrated various plugins into e-shops on the platform”Magento“, the number of different instances of the jQuery library that the plugins carried with them sometimes rose to a dozen. For backend applications compiled using npm, this would cause a version conflict, but in the browser – please. I am sure that this is the case when assembling the bundle It’s possible, but I think that the cost of resolving the issue will be slightly higher than simply indicating the version number in the export address.

Well, the most important advantage for me personally is the ability to work in a browser application with the same code as in the IDE. Yes, of course “sourcesmaps I wish they had already invented it“, but in my opinion, debugging with sourcesmaps is like the battle of Perseus with Medusa the Gorgon with its visualization through a polished shield. If Perseus could do without a shield, it would be stupidly faster.

Source code of modules in the browser

Source code of modules in the browser

Main disadvantages

The main disadvantages stem from the main advantages. Instead of downloading one large file into the browser, you have to download many small ones. This can be clearly seen in the example of the twing library:

File size and es bundle download time

File size and es bundle download time

In a compressed state, a bundle weighing 1.7 MB occupies 487 KB and takes 124 ms to load, of which 18.36 ms is spent waiting for the server to respond and 104.64 ms is loading the content:

Timing for the bundle

Timing for the bundle

Next after him util.js weighs almost 500 times less, and the loading time is only half as fast:

Timing for a separate module

Timing for a separate module

The main loss of time is waiting for the server to respond, which in the case of a large number of es-modules (files) adds up to a decent total time. This is somewhat mitigated by the presence of the browser disk cache:

Loading time of es-modules from disk cache

Loading time of es-modules from disk cache

and very strongly – by the presence of a cache in RAM:

Loading time of ES modules from RAM cache

Loading time of ES modules from RAM cache

If an application needs absolutely all modules for initial launch, then there is no point in downloading files individually; it is more profitable to download them as a bundle. But if the application allows modules to be loaded in batches of a couple of dozen pieces, then it makes sense to load them module by module.

Dynamic import

Advantages dynamic import before static is that we have the opportunity to link all our code not only at the stage of writing it, but also at the stage of its execution:

        const rnd = Math.floor(Math.random() * 2);
        if (rnd) {
            const {default: engine} = await import('https://unpkg.com/store-esm@3.0.0/src/store-engine.js');
            const {default: storages} = await import('https://unpkg.com/store-esm@3.0.0/storages/all.js');
            const {default: plugins} = await import('https://unpkg.com/store-esm@3.0.0/plugins/all.js');
            useStore(engine, storages, plugins);
        } else {
            const {
                TwingEnvironment,
                TwingLoaderArray
            } = await import('https://unpkg.com/@cloudfour/twing-browser-esm@5.1.1/dist/index.mjs');
            useTwing(TwingEnvironment, TwingLoaderArray);
        }

In the above example, either the package is downloaded in random order store-esm or package @cloudfour/twing-browser-esm.

Loaded sources for `rnd = 0`

Loaded sources for `rnd = 0`

As a result, with the help of dynamic import, we can not only reduce the amount of code loaded into the browser, but, more importantly, we can decide at runtime which implementation of the functionality to load – for example, password or fingerprint authentication. Or load UI components depending on the rights of the current user and plugins installed in the application.

In general, if the static import of ES modules in browser applications could be compared to moving on the surface of the earth, then dynamic import adds a third dimension to us – it gives us the ability to fly. In principle, nothing prevents us from using dynamic import in our single-page application any a package from those available on unpkg.com (if it is written in ES-modules and for the browser, of course).

“IoC over DI”

Well, since we “legalized“using ES-modules in the browser and even switched from”statically linking the code at the time it was written” To “dynamic at the time of its execution“, then there are only a couple of steps left before the final fall from grace – to the use of inversion of control in the code through dependency injection.

Here is the content of a typical es-module from those that I use in my applications:

export default class Fl32_Auth_Front_Mod_User {
    /**
     * @param {Fl32_Auth_Front_Defaults} DEF
     * @param {TeqFw_Core_Shared_Api_Logger} logger -  instance
     * @param {TeqFw_Web_Api_Front_Web_Connect} api
     * @param {Fl32_Auth_Shared_Web_Api_User_Create} endUserCreate
     * @param {Fl32_Auth_Shared_Web_Api_User_ReadKey} endReadKey
     * @param {Fl32_Auth_Shared_Web_Api_User_Register} endUserReg
     * @param {Fl32_Auth_Front_Mod_Crypto_Key_Manager} modKeyMgr
     * @param {Fl32_Auth_Front_Mod_Password} modPassword
     * @param {Fl32_Auth_Front_Store_Local_User} storeUser
     */
    constructor(
        {
            Fl32_Auth_Front_Defaults$: DEF,
            TeqFw_Core_Shared_Api_Logger$$: logger,
            TeqFw_Web_Api_Front_Web_Connect$: api,
            Fl32_Auth_Shared_Web_Api_User_Create$: endUserCreate,
            Fl32_Auth_Shared_Web_Api_User_ReadKey$: endReadKey,
            Fl32_Auth_Shared_Web_Api_User_Register$: endUserReg,
            Fl32_Auth_Front_Mod_Crypto_Key_Manager$: modKeyMgr,
            Fl32_Auth_Front_Mod_Password$: modPassword,
            Fl32_Auth_Front_Store_Local_User$: storeUser,
        }
    ) {...}
}

It has no static or even dynamic imports. The class constructor describes all the dependencies that objects of this class need to work. In this form, the es module can be used both in the browser and in a nodejs application – when calling the constructor, you need to create all the required dependencies and pass them through parameters. This can be done either by a programmer, manually, or Object Containerautomatically – according to the rules for converting dependency identifiers in the source path.

“IoC over DI” is a very old technology, almost ancient, one might say. Very widely used in PHP (Magento, Zend, Symfony…) and before that in Java (Spring). This is from what I personally used. PHP even has a standard for describing the object container interface – PSR-11. C# developers Mark Seemann And Steven van Deursen book about DI wrotewhere the pros/cons of the technology were explained in great detail and compared with other IoC approaches. In general, “IoC via DI” has long won and firmly established its reputation in many programming languages.

Taking into account the possibility of converting, depending on the context, some identifiers into others in the Object Container, as well as taking into account the possibility of post-processing of ready-made objects before their implementation, we can say that developing an application using DI is as much different from developing using dynamic import as Development using dynamic imports is different from developing using only static ones.

Converting dependency id to object before injection

Convert dependency id to object before injection

Conclusion

I don't really use transpilation to build a bundle of es modules, I load es modules directly into the browser. And to speed up this process I'm on a live server collecting I upload a regular zip archive of files that are supposed to be used on the front, download it to the client and unpack it into the browser’s cacheStorage. Then I use this cache in Service Worker when accessing esm.

Well, somehow it turned out that I have a bundle from esm, but no transpilation.

Disclaimer

The approach to loading ES modules described in this article is not generally accepted in the JS community and cannot be recommended for everyday use. Use it as you wish. All examples are far-fetched and have no practical use.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *