Managing Front-End Assets in Vapor, Part 3: Concatenating Files
In Part 2 of this series, we automated downloading front-end libraries using
npm. Once they were downloaded, we automated copying them into our
Public/ directory using
laravel-mix. This is a great start, but as you add more front-end dependencies to your app over time, you’ll run into a problem. Your app is pointing to each front-end asset individually, with one
<link> tag per stylesheet and one
<script> tag per script. As you add more front-end dependencies, you’ll need to add more
<script> tags, and your markup will get unwieldy.
Also, HTTP/1.1 (the version supported by Vapor) doesn’t handle a large number of parallel HTTP requests performantly. Each request opens a new TCP connection, which takes a while to ease into top speed.
Traditionally, the way to ensure your assets download quickly has been to concatenate all your separate JS files into one large JS file, and to do the same for your CSS files.
laravel-mix provides a
combine() command for doing just that. Let’s give it a try!
To follow along, download the complete part two project.
Reorganizing Your Files
Before we try the
combine() command, let’s rearrange our project files a bit to prepare for it. Concatenating files introduces a distinction between “source” and “generated” asset files. Source files are the ones we edit and commit to git. Generated files are the ones generated by our build tool and accessed by the end user.
Everything in the
Public/ folder will be served up by Vapor as a static asset, so that’s where generated files belong. Our source files won’t be accessed by the end user, though, so it’s best not to put them in the
Where else can we put them? Laravel, another web framework, provides a helpful pattern: it stores front-end source files along with view templates under a
resources/ folder. Vapor also has a
Resources/ folder for view templates, so let’s follow Laravel’s lead and add our front-end source files under there as well:
Resources/, create an
stylesfolders under that. Even though we currently only have one script file and one stylesheet, a real app would likely have multiple, so we set up directories for each.
Now that our files are organized, let’s update our
webpack.mix.js config to use the
combine() command to concatenate files:
-mix.copy([ - 'node_modules/bootstrap/dist/css/bootstrap.min.css', - 'node_modules/bootstrap/dist/js/bootstrap.min.js', -], 'Public/vendor/bootstrap/'); -mix.copy([ - 'node_modules/jquery/dist/jquery.min.js', -], 'Public/vendor/jquery/'); +mix.combine([ + 'node_modules/bootstrap/dist/css/bootstrap.min.css', + 'Resources/Assets/styles/app.css', +], 'Public/all.css'); +mix.combine([ + 'node_modules/jquery/dist/jquery.min.js', + 'node_modules/bootstrap/dist/js/bootstrap.min.js', + 'Resources/Assets/scripts/app.js', +], 'Public/all.js');
In addition to changing the command we use, note a few other differences:
- Previously there was a separate
copy()command for each library, because we were creating one folder per library. Now there’s a separate
combine()command for each file type, because we’re concatenating everything into one CSS file and one JS file.
laravel-mixdidn’t need to do anything with
app.js, because those files were already at their destination location under
laravel-mixdoes need to operate on these files, because they’ll be included in the concatenated output.
- We output the generated files directly under
Public/rather than in a
vendor/subdirectory. We no longer need a vendor and non-vendor distinction, because our generated files will include both vendor and non-vendor code. Our one CSS file and one JS file can just go directly under
Public/vendor/ folder will no longer be needed, remove it. Run
npm run build and look in
Public/—you should see
all.css. Note that it has Bootstrap code at the top, and the Vapor default styles from your
app.css at the bottom.
Next, remove the existing
<link> tags, and add a new
<link> tag pointing to
<head> <title>#import("title")</title> - <link rel="stylesheet" href="/styles/vendor/bootstrap.min.css"> - <link rel="stylesheet" href="/styles/app.css"> + <link rel="stylesheet" href="/all.css"> </head>
<script> tags in the same way:
#import("content") -<script src="/scripts/vendor/jquery.min.js"></script> -<script src="/scripts/vendor/bootstrap.min.js"></script> -<script src="/scripts/app.js"></script> +<script src="/all.js"></script> </body>
Reload your page in the browser. The Bootstrap tabs we set up in Part 1 of this series should still work! You should see something like this:
Previously we committed
Public/app.js to git because those were source files we edited directly. But the new
Public/all.js files are generated files, so let’s ignore them so they won’t be committed to git:
# Generated by `npm run build` -/Public/vendor +/Public/all.css +/Public/all.js
Why don’t we want to commit generated files to git? Committing them would make things a bit more convenient. That way, when you’re first setting up the app on a dev machine you won’t have to run
npm run build to make your generated files available.
One downside of committing generated files is that there’s a risk that someone will make changes to a generated file directly, commit the changes, and deploy it to production. The next time someone reruns the build, the change made to the generated file will be lost, but it won’t be clear why they disappeared. By contrast, if the generated file can’t be committed to git, then it’s more likely the developer would notice the changes missing from their git diff or from the test server, when it’s easier to correct the problem.
Another downside to committing generated fies is that any time you add, remove, or update a dependency, this will result in large diffs in your generated file, rather than just minor diffs in your
webpack.mix.js file. Because of this, front-end developers generally avoid committing generated files whenever possible.
Combining all your styles into a single
app.css works fine for most applications. But will a single
app.js file work? Apps often have very different scripts for different pages. There are a few different approaches you can take to handle this page-specific behavior. Here are three possibilities.
1. A single script file for the site.
This script file would be set up to skip any code that doesn’t apply to the page the user is currently viewing. Some simple jQuery operations make this easy because they operate on an array, so when no elements are found, there are simply no elements to perform the operation on, so it’s a no-op. For other jQuery operations or when you aren’t using jQuery, you can manually check whether DOM elements are present before you run an operation that requires them.
This approach results in a build configuration with fewer rules, because you’re only building one script file. It also speeds up subsequent page loads because the user has already downloaded and cached the script on their first page load. The downside is that the first page load is slower, because on that page the user has to download a large JS file with all of your site’s functionality, even if they never need most of it.
If performance is a major concern, or if you have a lot of page-specific script functionality, you may prefer to create:
2. Page-specific scripts.
Your main combined
app.js would only include global scripts. In some cases this only includes vendor libraries; in others it might also include a few site-wide scripts like page navigation. For anything page-specific, you could have separate
mix.combine() commands to set up scripts for that page, and include a separate
<script> tag on that page’s Leaf template.
This requires a build configuration with more rules, and it can mean that users have to download an additional script file on each page they go to. But it speeds up the first page load because users are only downloading the scripts they need for that page.
3. Upgrade to an infrastructure using HTTP/2
A third option is to find a way to use HTTP/2, which doesn’t incur the same amount of overhead for transferring numerous small files. Vapor itself doesn’t support HTTP/2, but it’s common practice to run a web application behind a web server like Apache or nginx, which can handle transferring static assets to the web browser over HTTP/2. In this approach, you wouldn’t need to concatenate your assets at all, but your markup would still be a bit cumbersome with a lot of
This is the final article of a series in which we’ve looked at a number of different ways to use third-party front-end library files:
- Accessing them on a CDN for speed and caching purposes
- Manually downloading them to our app to avoid depending on a third-party server
- Downloading them with
npmto ease version upgrades
- Concatenating them with
laravel-mixto minimize HTTP requests
Which approach should you take? If you’re just getting started in web development, it’s best to start with the simplest approach and only move to more complex approaches when you feel a need for the benefits they provide. If you haven’t been following along with the examples, start at Part 1 and give them a try so you get a feel for the different approaches. Once you’re familiar with them, you’ll be able to pull them out when you need them!
If you want to learn more about CSS and JS, attend our Front-End Essentials bootcamp. Our week-long intensive study will get you up to speed quickly.