Immutable Web Apps

Resolving dependencies when they are all bundled together is easy. Resolving dependencies when they are in being loaded via script tags is much more challenging. The goal of this talk is to explain how Meltwater handles dependency resolution when building native Web Component based applications that rely on packages published by many different teams.

Rate this content
Bookmark
Video Summary and Transcription
Immutable web apps are discussed, highlighting their ability to build once and deploy multiple times. These apps emphasize the importance of environment variables being configured outside the bundle for maximum asset caching and efficiency. The challenges of managing dependencies in web development are addressed, with a focus on the use of UMDs (Universal Module Definitions) to package code for different environments without increasing bundle size. The talk delves into tools like Webpack and Rollup, which help manage external dependencies by recognizing UMDs, and HiMyNameIs, which automates namespace mappings. Orchard CLI is also mentioned as a tool for organizing dependency loading orders based on a dependency tree. The concept of versioning in immutable web apps is explained, where assets are deployed to a URL with a version number for consistency across environments. The role of the index page in reflecting immediate changes and ensuring proper version control is covered. Arborist is introduced as a tool for dependency tree resolution, aiding in efficient script tag generation. The video also touches on the importance of static web hosting and APIs in the context of immutable web apps.

This talk has been presented at JSNation 2022, check out the latest edition of this JavaScript Conference.

Available in Español: Aplicaciones Web Inmutables

FAQ

Immutable web apps are applications built with the philosophy of building once and deploying multiple times. They aim to have their environment variables configured outside the bundle to ensure that each deployment is immutable, meaning once a version is built, it never changes. This allows for consistent testing across environments and maximizes asset caching.

Dependencies become problematic when there are too many being used by different applications, leading to duplicated JavaScript and large bundle sizes. This can slow down application performance and complicate updates and maintenance.

Immutable web apps deploy assets to a URL that includes the version number. This approach ensures that assets are consistent across different environments and are maximally cached, reducing load times and bandwidth usage.

UMDs, or Universal Module Definitions, allow developers to package code in a way that it can be used as a module in different environments. In web apps, they help manage dependencies by allowing them to be loaded globally without bundling them into the main codebase, thus reducing bundle size and duplication.

Tools like Webpack and Rollup are used to manage external dependencies by configuring them to recognize UMDs. Additionally, tools like HiMyNameIs help in generating namespace mappings, and Orchard CLI organizes the loading order of dependencies based on a dependency tree.

HiMyNameIs automates the mapping of module names to global namespaces, reducing maintenance overhead. Orchard CLI, on the other hand, uses dependency trees to determine the correct loading order of scripts, ensuring that dependencies are loaded efficiently and correctly.

By using the same assets in both staging and production environments, immutable web apps ensure that any configuration changes are the only differences between the two. This consistency helps in reducing bugs and issues that might arise due to environment discrepancies.

1. Introduction to Immutable Web Apps#

Short description:

Today we're going to talk about immutable web apps and dependencies. We had a problem with dependencies at Meltwater, so we needed to find a way to unbundle them and share them effectively. Immutable web apps allow us to build once and deploy many times, with environment variables outside the bundle. This ensures that the assets can be maximally cached, resulting in faster loading times. It also provides easy version tracking and rollbacks.

How's it going JS Nation? Today we're going to talk about immutable web apps and dependencies. First, I'm Andy Damaris, you can find me on pretty much all the social medias at Teradox and my website is teradox.tech.

What we're going to cover today is, we're going to start with the problem. There was a problem that we were having at Meltwater and then we'll move on to immutable web apps, what they are and why we're using them, dependencies without bundling them, referencing those dependencies and then last we're going to talk about how we or those dependencies successfully.

So, what's the problem? Well, the problem was dependencies. We had a lot of them and they were being created by internal teams for libraries that we could use on a regular basis for things like authentication or companies lookups or users lookups and each of these things was beginning to compound the amount of duplicated JavaScript that was being bundled together with all of the different applications we were shipping. So, we needed to figure out a way to unbundle these dependencies and allow them to be shared more effectively between the many applications that were using them.

So, immutable web apps were an important part of that journey, so we're going to cover those first. The basic philosophy of immutable web apps is that you want to build them once and deploy them many times. There are some really specific fundamentals that we need to accomplish in order to be an immutable web application. The first one is all of our environment variables need to be outside of the bundle. This allows our bundles to be built one time per version and be immutable for that specific version. They'll never change after the first time they're built. It also means that we need to deploy them to a URL where the version that we just built is included as a part of that URL. So we want that fully qualified URL to have our version number in it somewhere.

Now, what benefits does this really buy us? There's a bunch. The first is that when we're testing in staging and we're testing in production, they're using the exact same assets. The only difference will be the configuration that's changed between the two. This means that all of our assets can also be maximally cached. They can be cached for a full year in fact, which is the current browser maximum. There's a huge benefit to our customers for that. They only ever need to round-trip for that specific version of that specific asset once. And after that, it's on their local disk cache, saving huge amounts of time for secondary and tertiary loads of the site. We never have to worry about them having to go back to origin constantly for these large assets at times. The other things that get included with this are you always know which versions are deployed because your index.html page makes it very plain. Look at your script tag. What version's in the URL? That's the version you're dealing with. It also means that rollbacks are now really trivial. We're just flipping back from one specific version of a set of assets to another script tag. Another specific version of a set of assets.

2. Immutable Web Apps and Dependencies#

Short description:

If you're a consumer, chances are good you already have the previous version of assets in your disk cache. The index page is the focal point for immediate changes. We break things down into three thought processes: static web hosting, delivering static assets using script tags, and APIs. To hit the API, we use a configuration block in the script tag. We have a lot of dependencies, so we need tooling to fix the problem. The first tool is UMDs, the Universal Module Definition style of bundling.

And if you're a consumer already, chances are good you already have that previous version of assets in your disk cache ready to go. So you won't even have to pay download costs again for it.

So we've talked about all of our assets but what about the index page? Well, the index page is the one part of an immutable web app that you don't cache. It's our focal point for where all of our changes are allowed to show up immediately.

So we like to break things down into three different thought processes. You don't have to break it down this way. It's just an exercise in a way to think of things. So the first piece is just our static web hosting. It hosts our index HTML page. That index HTML page is pretty static. There's not a whole lot of dynamic nature to it. And it dictates the versions of all of the static assets that we're going to deliver. Those static assets are then delivered using script tags. And those script tags use the fully versioned URL that we were talking about. In this case, version 1.2.3 to be able to deliver those assets.

Then we also have our APIs, and our APIs could be on a different server, could be on the same server. But we dictate which API we're going to hit by that configuration block you're seeing in the script tag. That configuration block tells our application where we should go to hit that API and what the route should be, removing that environment variable from our JavaScript code.

So I've lost over immutable web apps pretty quickly here. There's some more nuance, a lot more detail that you could dig into. And if that's something that's interesting to you, please check out immutablewebapps.org.

So now let's dig back into the main crux of what we're talking about, which is dependencies. We have a lot of them. They're being used by a lot of different applications. So how do we fix that problem? Well, we're going to need some tooling to do this successfully. The first little bit of tooling we're going to use is UMDs. They're a specific type of bundle that we'll dig into. And then we're going to talk about HiMyNameIs, a tool for helping us discover dependency names. And then we'll talk about Orchard, which is really our tool for ordering those dependencies. So the first tool I want to discuss is UMDs, the Universal Module Definition style of bundling.

3. Using UMDs and Bundling Tools#

Short description:

Pretty much every bundler out there supports it, and it allows those bundlers to give a specific name to a package of code that will then end up on the global disk scope in the browser. The example that we're going to work with today is Meltwater Visualizations. When you have a lot of intertwined dependencies, not all of them are going to be able to move at the same pace. Upgrading a dependency for, like, visualizations to version 14 when other people are relying on version 13 might mean that everyone has to upgrade at the exact same time and do a very large forklift upgrade. But if you can load Meltwater Visualizations 13 and Meltwater Visualizations 14 at the same time, you give those other teams an opportunity to upgrade at their own pace, at a reasonable pace that makes sense for the workload that they're under. And that means that versions can move more slowly. And instead of having to do a forklift upgrade, you can now upgrade versions when they make sense to do so for your team's movement. So how do we use UMDs within our bundle? Referencing them comes down to bundling tools. We'll start with Webpack. Webpack has a property called externals. Externals allow us to load those UMDs that have been put on the global disk scope by referencing them using their module name. By using their module name it tells Webpack, I don't want to bundle this node module anymore. Instead, I want to make a reference to that node module to this corresponding global disk namespace. The benefit of doing it this way is that when we're testing with something like Jest or VTest, we can still use that node module to be able to run our tests. It doesn't have to reference the browser specific code if we don't want it to. This means that our testing and mocking is much more straightforward than if we were always relying on the global disk namespace. Now let's look at what a rollup config could look like for this. In rollup, those two things we were talking about that Webpack handles in the externals get divided out into two separate configuration options.

Pretty much every bundler out there supports it, and it allows those bundlers to give a specific name to a package of code that will then end up on the global disk scope in the browser. The benefit of this is that now we can reference that module using the global disk namespace that it's occupying in order to bring it in without having to bundle that dependency into our other code.

The example that we're going to work with today is Meltwater Visualizations. And you'll notice that when we're talking about making a name for this UMD bundle, we include that suffix of a V major version. In this case, major version 14 of Meltwater Visualizations. The reason for this is that when we reference a major version, it allows us to potentially load multiple major versions of the same dependency in the page at the same time. Now, you might be thinking that that feels like a bad idea, and I agree with you. But there's also benefits to being able to do that. When you have a lot of intertwined dependencies, not all of them are going to be able to move at the same pace.

Upgrading a dependency for, like, visualizations to version 14 when other people are relying on version 13 might mean that everyone has to upgrade at the exact same time and do a very large forklift upgrade. But if you can load Meltwater Visualizations 13 and Meltwater Visualizations 14 at the same time, you give those other teams an opportunity to upgrade at their own pace, at a reasonable pace that makes sense for the workload that they're under. And that means that versions can move more slowly. And instead of having to do a forklift upgrade, you can now upgrade versions when they make sense to do so for your team's movement.

Now, this doesn't mean you shouldn't be mindful of a lot of duplication of code, that's how we ended up here in the first place. But it does mean that it makes that upgrade path a lot smoother across a lot of different teams. So how do we use UMDs within our bundle? Referencing them comes down to bundling tools. We'll start with Webpack. Webpack has a property called externals. Externals allow us to load those UMDs that have been put on the global disk scope by referencing them using their module name. By using their module name it tells Webpack, I don't want to bundle this node module anymore. Instead, I want to make a reference to that node module to this corresponding global disk namespace.

So a couple of things are happening here. One, Webpack is being told to ignore bundling that module name, and two, that module name is being set up to correspond to a global disk namespace object that should exist. The benefit of doing it this way is that when we're testing with something like Jest or VTest, we can still use that node module to be able to run our tests. It doesn't have to reference the browser specific code if we don't want it to. This means that our testing and mocking is much more straightforward than if we were always relying on the global disk namespace. The benefit there is easier test setups, easier to debug, and your local code runs the way that you would expect to. It's wonderful to know that we can just use the NPM module for local testing and know that that will act the same as when we're deployed.

Now let's look at what a rollup config could look like for this. In rollup, those two things we were talking about that Webpack handles in the externals get divided out into two separate configuration options.

4. Bundling Modules in Rollup#

Short description:

The external property in rollup allows us to specify which modules should not be bundled. We can use the output global's option to map module names to global disk namespaces. This makes it easy to reference UMDs from the window object.

The first one being which modules are not going to be bundled together with us. That's what the external property is for in rollup. It says, here are the modules, I just don't want you to load. You don't need to bundle those into my bundle. And then we can, down in the output global's option, use that same type of map we saw in Webpack, where we referenced the left hand side is the module name, and the right hand side is the global disk namespace that we want to resolve for that module name while we're bundling. It has a very similar output in the way that we are referencing these things, and it makes it very straightforward to reference UMDs from the window object.

5. Automating Dependency Configuration and Ordering#

Short description:

Maintaining the configuration of multiple dependencies can become exhausting, especially when there are many to keep track of. However, the package HiMyNameIs offers a solution by automating the process of building the module to namespace mapping. It provides two functions: generateNamespaceFromPackage and getPackageNamespaceMapping. These functions allow you to easily build the global namespace and resolve dependencies in your package.json. Additionally, the Orchard CLI tool automates the ordering of dependencies, ensuring they are loaded in the correct order for global resolution.

But if we start thinking about when we get to 20 or 30 or 50 of these dependencies, maintaining that config is exhausting. There's so much to maintain. Every single one of them has a major version we have to keep track of. When we upgrade our package.json, that major version could change and we have to remember to update it in the config. I mean, it gets to be a bit of a mess, but luckily we can automate that and we can automate it through a package called HiMyNameIs. This package is going to be open-sourced, hopefully within the next month and you'll be able to take advantage of it.

HiMyNameIs is basically a library that allows us to build out that module to namespace mapping that we were talking about without you having to maintain that yourself. And it does it with two very straightforward functions. The first function is called generateNamespaceFromPackage. generateNamespaceFromPackage allows you to build that global this namespace we were talking about including our major version so that we can populate it into a new package.json property. That package.json property is called browserNamespace. This property gives us the ability to resolve that information later in the build. So once you've got a library that's using this and building out that property and package.json, it means your consumers are now set up to be able to successfully reference them. Let's look at what that looks like.

The second function out of HiMyNameIs is getPackageNamespaceMapping. This function goes to your package.json for your project and looks at all of the dependencies. It ignores the dev dependencies and resolves those dependencies to look at their package.jsons. If the package.json for that dependency has a browserNamespace property in it, it uses that to build out the mapping of modulename to globalthisNamespace. So you don't have to maintain that anymore. As you're versioning, it'll just get versioned for you. Every time you build, this will resolve appropriately in a very rapid manner and give you that mapping that you need. So in rollup, it's a little bit more complicated, but not much. Similar to with webpack, the getPackageNamespaceMapping function still gives you that globals object, but then you also need to build out that external array, like we referenced earlier. Luckily, the getPackageNamespaceMapping has all the information you need to be able to do that successfully.

Okay, but we still haven't tackled ordering, right? And that's a much harder problem, because dependencies can have dependencies can have dependencies, and they all need to be loaded in the right order in order for this global this resolution to work appropriately. But we can automate that too. That's where the Orchard CLI tool comes in. It very similarly to hi my name is reads your package, Jason, looks only at your dependencies, ignores dev dependencies. But instead of stopping at the shallow level this time. It uses a package directly out of NPM JS called Arborist to build out the full dependency tree.

6. Dependency Tree Resolution and YAML Files#

Short description:

Arborist is a powerful tool that handles dependency tree resolution for NPM install and helps build out the package lock Jason. It builds script tags based on the dependency tree resolution and limits them using a curated set of YAML files. These files contain ownership information, technical details, and allow for the creation of ordered dependencies. The orchard keeps track of who is responsible for each dependency, and the YAML files specify the path and version for each dependency. The system also handles conflicts with other major versions and requires initialization.

Arborist is basically behind the dependency tree resolution for things like NPM install. It allows NPM install to build out that full dependency tree to understand what new packages need to be put where in that dependency tree. It helps build out the package lock Jason as well. So very powerful tool that we did not build in order to make this system function.

Using that dependency tree, it then builds out a set of script tags based on that dependency tree resolution. And then we limit that using a curated set of YAML files. Why a curated list of dependencies? Well, there's a couple of reasons. The first one mostly is safety. We don't want just any package being loaded from node or loaded from NPM. We really wanna limit it to just things that actually we want to be loaded into the browser. By limiting that, we're kind of limiting the number of things that can load to a very specific subset that we actually care about. It also helps by limiting that dependency tree resolution to a much shorter amount of time by trimming branches that are no longer relevant.

So with these tools in place, we have this YAML file now that we need to worry about with the orchard. Luckily, it's a reasonably straightforward YAML file. Here's what one looks like for an internal dependency. At the top, we have ownership information. It's wonderful within a large organization to be able to know exactly who is responsible for the dependency that you're relying on in production. That's a part of what the orchard allows us to keep track of. Each of these YAML files has an owned by, a repo, and a contact property that allows us to keep really close track of who is actually in control of these different dependencies. Then under the technical details, this is where we're actually building out the path for each of the dependencies that we're loading either via script tag or link tag. We split it into these three parts for a really specific reason. We wanted a suffix that kind of is the base path that these things will be loaded from. Then we wanted a version path that allows that version path to be maybe prefixed with a v or an at symbol depending on where it's being loaded from. Then the suffix is the specific files that need to be loaded. In the case of what we're looking at, this would load a script tag with type equal module and then resolve out that base path plus the version plus the ESM path as the source for that script tag. It would also create a link tag based on that once again, that base path, the version and the CSS path to build out that link tag allowing us to create a set of ordered dependencies. There are two other properties in here that are worth calling out. One is conflicts with other major versions which is that call-out before of the fact that we try to make sure that our internal libraries can run with multiple major versions at the same time. And the last one here is requires initialization.

7. Initializing Internal and External Dependencies#

Short description:

There are internal libraries that require initialization code for appropriate state. External dependencies, like moment.js, have similar ownership and technical details. ES5 is used instead of ESM, with a script tag and defer attribute. Conflicts with other major versions are not allowed.

And the last one here is requires initialization. There are some internal libraries that require you to run some initialization code in order for you to be in an appropriate state. And we like to call that out for our consumers so that they know exactly what they need to bootstrap. Looking at an example for an external dependency looks very, very similar to an internal dependency. In this case, moment.js. We can see that the ownership stuff has shifted a little bit. It's no longer an internal concern so we don't have things like team name. But we do have things like the repo that it came from and where I can go to contact them if I need to in case of an issue. The technical details are also very similar. In this case, you can see that we're using ES5 instead of ESM because there isn't an ESM version of moment.js. So all that means is that instead of being a type equal module script tag, we'll now get a script tag with a defer on it, which basically allows that script tag to be deferred until after all of the HTML has been parsed and it will follow the same ordering process as a type equal module for when we're doing that resolution. You'll see here that we have conflicts with other major versions set to true. You can't load multiple major versions of moment. It's just not allowed. They occupy the same global this namespace and would overwrite one another. So that's a couple of examples of YAML files that are used to configure the orchard.

Andy Desmarais
Andy Desmarais
20 min
20 Jun, 2022

Comments

Sign in or register to post your comment.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

pnpm – a Fast, Disk Space Efficient Package Manager for JavaScript
DevOps.js Conf 2022DevOps.js Conf 2022
31 min
pnpm – a Fast, Disk Space Efficient Package Manager for JavaScript
Watch video: pnpm – a Fast, Disk Space Efficient Package Manager for JavaScript
pnpm is a fast and efficient package manager that gained popularity in 2021 and is used by big tech companies like Microsoft and TikTok. It has a unique isolated node module structure that prevents package conflicts and ensures each project only has access to its own dependencies. pnpm also offers superior monorepo support with its node module structure. It solves the disk space usage issue by using a content addressable storage, reducing disk space consumption. pnpm is incredibly fast due to its installation process and deterministic node module structure. It also allows file linking using hardlinks instead of symlinks.
Yarn 4 - Modern Package Management
JSNation 2022JSNation 2022
28 min
Yarn 4 - Modern Package Management
Top Content
Yarn is a package manager that focuses on stability, performance, and security. It offers unique features like plug and play installation, support for nonmodules, and the exec protocol. Yarn is committed to being a good citizen in the open-source community and contributes to fixing dependencies. It is part of the Node.js Loader's working group and advocates for Corepack. Yarn is still experimental but is improving its user experience and security features. Contributions are welcome, and switching to Yarn can improve performance in large projects.
The Good, The Bad, and The Web Components
JSNation 2023JSNation 2023
29 min
The Good, The Bad, and The Web Components
Top Content
Web Components are a piece of reusable UI enabled by web standards and built into the web platform. They offer the potential for faster component initialization and less library overhead. Web Components can be created from scratch and utilized with existing component libraries. Shadow DOM and Declarative Shadow DOM provide benefits such as scoped CSS and server-rendered components. The tradeoff between not repeating oneself and achieving full server-side rendering support is discussed. User experience is deemed more important than developer experience.
It's Time to De-Fragment the Web
React Day Berlin 2022React Day Berlin 2022
34 min
It's Time to De-Fragment the Web
Top Content
Today's Talk introduces Mitosis, an open source project that solves the problem of building framework agnostic components. It supports JSX and Svelte syntax and outputs human-readable code for various web frameworks. Mitosis is already being used by enterprise customers and can be easily integrated with React, Svelte, and other frameworks. It saves time, allows for easy handling of framework version migrations, and enables efficient unit and integration testing.
Web Components, Lit and Santa 🎅
JSNation Live 2021JSNation Live 2021
28 min
Web Components, Lit and Santa 🎅
Web Components and the Lit library are introduced, highlighting their ability to create custom elements and replicate built-in components with different functionality. The use of semantic HTML and the benefits of web components in development are emphasized. The features of Lit, such as data binding and rendering, are discussed. The Santa Tracker is showcased as an example of web components being used in educational games. The compatibility of web components with other frameworks and their versatility in creating small widgets or large applications are highlighted.
Authoring HTML in a JavaScript World
React Summit US 2023React Summit US 2023
21 min
Authoring HTML in a JavaScript World
Watch video: Authoring HTML in a JavaScript World
This Talk by Tony Alicia focuses on authoring HTML in a JavaScript world. The speaker challenges developers to change their approach to building React components by starting with HTML first. By authoring HTML in a semantic way, readability and maintainability can be improved. Well-authored HTML provides better understanding of content and improves accessibility. It also has performance benefits by reducing DOM size. Investing time in HTML can save time and make applications more future-proof.

Workshops on related topic

Web Components in Action
JSNation Live 2021JSNation Live 2021
184 min
Web Components in Action
Workshop
Joren Broekema
Alex Korzhikov
2 authors
The workshop extends JavaScript programming language knowledge, overviews general software design patterns. It is focused on Web Components standards and technologies built on top of them, like Lit-HTML and Lit-Element. We also practice writing Web Components both with native JavaScript and using Lit-Element. In the end we overview key tooling to develop an application - open-wc.