Lazy Loading Custom Assets with StencilJS - Part 1

Graffiti Wall with Woman's Face
Lisa Backer

Engineer

Lisa Backer

I’ve been doing some work in StencilJS lately, a totally standards based way to create web components that can then be used by any framework, or no framework at all. Created by the Ionic team, StencilJS powers the Ionic framework — a robust framework of components that enables a single codebase to power many platforms. Stencil allows you to write in TypeScript with asynchronous rendering, one-way data binding, and generates components with dynamic polyfills and lazy-loaded assets. The result are web components that you can use in any web application that work with today’s (and even yesterday’s) web browsers. It is still beta software, however, and at times I find things can be harder than I expected.

TL;DR

Lazy loading your own assets in Stencil is possible, but can require a little extra code and configuration both for web and distributed builds. This post details the necessary steps for a web build. A post to follow will detail the steps necessary for distributed builds (installed via npm).

The Scenario

I have a web component that needs to display localized text. Once the component determines the appropriate language to show at runtime, it needs to lazy-load the JSON-formatted translation files for that language. These translation files are generated at build time into a local “locales” folder within the root of my project. No problem because Stencil does lazy loading already out of the box – it can’t be that hard.

The Reality

Asking around the Stencil slack (a great resource of helpful developers) I was met mostly with a response of either “can’t you embed your assets in the build like svg assets” or “maybe just include all the languages up front.” Neither of these solutions is an option for me. There are too many languages supported to take that performance hit up front, and JSON files just don’t work the same way as image assets. One helpful soul said they didn’t know the answer, but pointed me towards Ionic’s own icon library which manages to lazy-load icon files from either a web build or a distributed npm installation. I decided to work my way through using that as inspiration.

Web Builds

Let’s assume an application namespace of “muppet-fans.” Stencil will build your output for a web server into the “www” directory with a structure of:

|--www
  |--assets
  |--build
     |--muppet-fans
       |-- component-files.js
     |--muppet-fans.js
  |--index.html

Copying Your Assets To Build

Stencil provides two ways to include additional files in the build. The first is the assets directory which is primarily used for images referred to within CSS. The second is a copy configuration to copy assets from any location to your build directory. These assets could be from any custom location including your node_modules folder.

For example:

copy: [
  { src: '/locale/*.js', dest: ‘build/muppet-fans/locales’ },
  { src: '../node_modules/moment/locale/*.js', dest: ‘/build/muppet-fans/moment-locales’ }
]

Would copy all of my custom translation files from /locale as well as the moment.js locale files from my node_modules folder into the www/build/muppet-fans/locales and www/build/muppet-fans/moment-locales folders, respectively. From there your component code could then refer to them with relative paths.

|--www
  |--assets
  |--build
     |--muppet-fans
     |-- locales
       !-- lang1.json
       !-- lang2.json
     |-- moment-locales
       |-- lang1.json
       |-- lang2.json
     |-- component-files.js
     |--muppet-fans.js
  |--index.html

This will all work well on local development when all of your files are loaded from the same domain, but what about in a distributed component case when you component is served from a different domain than the script host?

Resources URL and Lazy Loading

Stencil provides a context property to access the resourcesUrl. This is a configurable property that tells the loader where to find your components. This is the magic of how Stencil is able to lazy load components. When defined, this is the path used to load your assets. When not defined, it simply defaults to the build location.

You can get a reference to this property in your components:

@Prop({ context: 'resourcesUrl' }) resourcesUrl!: string;

And then we could use this to load a language-specific locale file with:

async updateLocale(lang) {
   const response = await fetch(`${this.resourcesUrl}locales/${Lang}.js`);
}

There are two immediate problems with using this value. 1) It only gives you a relative path when not configured explicitly in the stencil.config file; and 2) the Stencil team has been saying all over the Slack channels not to rely on contextual properties anymore as this functionality may be deprecated soon.

In a simple case it seems that the intention would be to configure this value to be relative when developing locally, but absolute in a production build. In the “real” world, however, this isn’t so simple. You may be running automated tests against a production build in a test environment that requires access to the test version of your assets. Obviously Stencil itself is able to lazy load JavaScript assets correctly whether local or remote, and so I set out to find out how so that I could use this approach for my locale-specific assets.

When a Stencil component is added to a site, a loader script adds a data attribute to the main script file with the absolute location of the assets to load. This data attribute is then read to lazy load any further assets. We can read this data attribute from our own code to get the resources url to prepend to any lazy-loaded assets.

For example:

<script src="https://muppetationalhost.com/muppet-fans/muppet-fans.js" type="module" data-resources-url="https://muppetationalhost.com/muppet-fans/" data-namespace="muppet-fans"></script>

Now when loading our asset files:

async updateLocale(lang) {
   const resourcesUrl = document.querySelector('[data-resources-url]') || this.resourcesUrl;
   const response = await fetch(`${resourcesUrl}locales/${Lang}.js`);
}

Now our locale files can be lazily loaded from the same location as our build JavaScript assets regardless of environment. This only took a little configuration to copy the assets to the build and a slight change in the way we reference the file location.

Dist Builds?

What’s coming next? The copy configuration only affects the web build output. How can we lazy load the translation files from a build distributed via npm? Iconic icons can do it, so we can too.

DockYard is a digital product agency offering exceptional strategy, design, full stack engineering, web app development, custom software, Ember, Elixir, and Phoenix services, consulting, and training. With a nationwide staff, we’ve got consultants in key markets across the United States, including Seattle, Los Angeles, Denver, Chicago, Austin, New York, and Boston.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box