DEV Community

kj for Begin

Posted on

Progressive Bundling Example

This is a follow-up code example for this post introducing Progressive Bundling: Progressive Bundling Post.

Check out the live example here: Live Example

Let's step through a project that has progressive bundling enabled to see how you might be able to use this approach in your next project.

You can follow along at home with this repo: begin-examples/node-progressive-bundling

Alternatively you can deploy your own on Begin by clicking this link: Deploy to Begin

To see the Progressive Bundling pattern in action, open up your browser's development tools to watch the network as the page loads.

Watch for a request to be made for home.js, and you should see a 302 redirect to a bundled and fingerprinted version of that module.

302 redirect to bundled module

Project overview

Now that we've seen it in action let's get acquainted with the project. This is a standard Architect serverless project and while you don't need to know Architect to follow along you might want to have arc.codes open in case any questions come up.

Let's start at the beginning and look at how to configure the project's infrastructure.

.arc

The project's .arc file is how we declare this app's infrastructure as code. We declare all of the app's HTTP Functions under the @http pragma.

@app
pb-example

@static
folder public

@http
get /
get /about
get /modules/:type/:module
get /cache
post /cache

@tables
data
  scopeID *String
  dataID **String
  ttl TTL

folder structure
├── src/
│   └── http/
│   │    └── get-index/
│   └── views/
│        └── modules/
│   └── shared/
└── .arc
src/http

This folder is where all the HTTP function's source code lives. Architect maps these HTTP Functions to the routes declared under the @http pragma in the app's .arc file.

By convention, get-index/ is mapped to the route '/' and serves https://yoursite/. Similarly, get-about is assigned to /about and serves https://yoursite/about. A post-cache route would accept posts from forms having the action attribute set to action="/cache".

src/shared

This folder is where you put code you want to share between functions. Any code placed inside src/shared will be available to require from @architect/shared.

src/views

This folder is where all of your shared view code lives. These files are copied to all the GET routes by default allowing you to share view code throughout your app. Any code placed inside src/views will be available to require from @architect/views.

Implementation

This example app implements the Progressive Bundling pattern with a few discrete steps.

  • cache lookup: First, we check to see if the file already exists in the cache. If it does, we return it.
  • bundle: Then if the requested file isn't already in the cache, we bundle it.
  • fingerprint: Next, we generate a fingerprint id and append it to the file name.
  • cache Then this new file is cached for subsequent requests.
  • redirect: Finally, we redirect to the newly cached file.

It is possible to create a fingerprint by using a hash of the bundled file's contents. Appending a fingerprint to the file name ensures that the file name is updated when the content of the bundle changes. Adding a fingerprint to the file name avoids getting an outdated file due to a cached version in the end user's browser or network.

Now that we know the steps let's follow a request for a module through to the bundled response.

The route we are going to focus on first is get /modules/:type/:module. This route passes a module type and module name to our HTTP Function as parameters on the request.

The folder for this route is generated by Architect the first time you start the sandbox server. The folder for this route replaces the colons ":" that defines a route to a system-friendly three zeros "000". This means that the route get /modules/:type/:module will generate the folder src/http/get-modules-000type-000module.

src/http/get-modules-000type-000module

// Check to see if file is in cache
let file = await read({ name })
// Bundle the file if it is not found in the cache
if (!file) {
  file = await bundle({ name })
}
// Redirect to the file
return redirect(`/_static/${ file }`)

The above code is where all the action is in this file. This code first looks to see if there is a cached version to send; if not, it bundles the requested file then redirects the request to the bundled version.

Let's look at how we have implemented the cache.

src/shared/cache-read.js

const data = require('@begin/data')
// check the cache manifest
let cache = await data.get({
  table: 'module-cache',
  key: name
})

In the previous code block, we look for the module by name in a table called module-cache then return the file if found. In this example, we use Begin data for simplicity, but any data store would work.

Next is the code responsible for bundling the file.

src/http/get-modules-000type-000module/bundle.js

// Get the path to this module on disk
let input = path({ name })
// Pass the file path to rollup
let bundle = await rollup.rollup({ input })
// Bundle together modules
let bundled = await bundle.generate({ format: 'esm' })

Above we look up the file and then pass it to rollup to bundle. Rollup was used in this example, but you could substitute the bundler of your choice.

One last stop at the redirect code before sending the response.

src/http/get-modules-000type-000module/302.js

module.exports = function redirect(location) {
  return {
    statusCode: 302,
    headers: { location }
  }
}

Pretty straight forward, but it's useful to see how to send any type of reponse with a status code.

Wrapping up

We stepped through a project implementing this pattern and looked at all the places this pattern can be customized to fit your own needs. This example illustrates how Progressive Bundling can be implemented but is by no means the end. You can apply this pattern in your way and with other outputs. We can't wait to see what you make!

Top comments (0)