Upgrading Symfony Websites to AssetMapper

During the past weeks, we’ve been upgrading Symfony websites like
symfony.com, live.symfony.com
and certification.symfony.com to use
AssetMapper instead of Webpack Encore. This blog posts explains how we did it.

Initial Situation: Webpack Encore

All Symfony websites used Webpack Encore to manage their assets. Encore is a small
layer on top of Webpack to make it easier to configure and manage. This setup
worked well for us, but it has some drawbacks.

First, it requires installing, configuring and maintaining a non-trivial
JavaScript setup with tools like Node.js, Babel, Webpack, etc. Thanks to Webpack
Encore, a lot of this complexity is transparent to you.

However, when deploying the application, we had to replicate this system in
production. We use Platform.sh to deploy our websites, where the building of
assets with Webpack Encore is automated. But having to build assets on every
deploy makes the deployment slower (and consumes resources unnecessarily).

The second important drawback of Webpack Encore is that you need to build assets
before using them (with commands like npx encore production). When developing
the application in your local machine this is cumbersome, even if you can run a
command like npx encore dev –watch to rebuild assets automatically when you
change anything on them.

After the introduction of AssetMapper in Symfony 6.3 and the AssetMapper improvements
in Symfony 6.4, we decided to give AssetMapper a try. The promise sounded almost magical:
you could have all the good parts of Webpack Encore and none of the bad parts and
have them in a much simpler way.

Unlearning

Before continuing, it’s important to unlearn some things. Many web developers agree
on this: „a best practice is to use a bundler (like Webpack) to combine and minimize
assets before serving them“
. This is no longer true.

You don’t need a bundler to do great and complex things in JavaScript involving
imports. All browsers support imports natively now. Read more about this
You don’t need to minify the contents of web assets (to remove white spaces,
transform CSS properties to optimize them, etc.) Compressing the web assets
in your server before serving them gives you almost the same result.
Read more about this
You don’t need to combine assets into a big single asset to reduce the number
of HTTP requests. When using HTTP/2 and HTTP/3, it’s fine to send tens of asset
files to the browser. You can still get a 100/100 speed score.
Read more about this

Now we’re ready to do the actual migration to AssetMapper.

Migrating from Webpack Encore to AssetMapper

For a real and full example of migrating an application from Webpack Encore to
AssetMapper, check out this pull request
in the Symfony Demo repository.

Installation

First, install AssetMapper as explained in the AssetMapper documentation (for
us, it was just running composer require symfony/asset-mapper command and
checking the changes done by the associated recipe).

The importmap.php File

Instead of using the webpack.config.js file, in AssetMapper you use a file
called importmap.php at the root directory of the project. This file tells
which assets are used in your application.

Instead of creating this file by hand, you run the importmap:require command
and add all the needed assets. In our case, we took the list of assets in the
package.json file and added the dependencies one by one with the
importmap:require command:

$ php bin/console importmap:require bootstrap
$ php bin/console importmap:require clipboard
$ php bin/console importmap:require tom-select
# …

Keep in mind that you don’t have to install the dependencies that were used only
by Webpack Encore and not your application (in our case: @babel/core,
@babel/preset-env, file-loader, webpack, etc.)

If you open the importmap.php after installing the assets, you’ll see something
like this:

<?php

return [
// …
‚bootstrap/dist/css/bootstrap.min.css‘ => [
‚version‘ => ‚5.3.2‘,
‚type‘ => ‚css‘,
],
‚tom-select‘ => [
‚version‘ => ‚2.3.1‘,
],
‚clipboard‘ => [
‚version‘ => ‚2.0.11‘,
],
// …

This file only defines the versions of the assets. To actually download the assets,
you must run the importamp:install command, which downloads the assets to
the <your-project>/assets/vendor/ directory. This is like the old node_modules/
directory, but if you compare both, you’ll see that the new assets vendor only
contains a few files. This is because AssetMapper downloads the full compiled
CSS/JavaScript files, instead of all the source files needed to build them.

The Entrypoints

In the webpack.config.js file you defined the Webpack Encore entries,
which are the final asset files that will be generated when building assets:

Encore
// …
.addEntry(‚app‘, ‚./assets/app.js‘)
.addEntry(‚admin‘, ‚./assets/admin.js‘)
.addEntry(’schedule‘, ‚./assets/conference-schedule.js‘)
;

In AssetMapper there’s a similar concept called entrypoints. When upgrading
to AssetMapper, we mapped the entries into entrypoints 1-to-1. You need to
do this manually by editing the importmap.php file:

<?php

return [
‚app‘ => [
‚path‘ => ‚./assets/app.js‘,
‚entrypoint‘ => true,
],
‚admin‘ => [
‚path‘ => ‚./assets/admin.js‘,
‚entrypoint‘ => true,
],
’schedule‘ => [
‚path‘ => ‚./assets/conference-schedule.js‘,
‚entrypoint‘ => true,
],
‚bootstrap/dist/css/bootstrap.min.css‘ => [
‚version‘ => ‚5.3.2‘,
‚type‘ => ‚css‘,
],
// …

We realized that we probably have too many entrypoints. This is because, in the
past, we wanted to load on each page the smallest amount of CSS/JavaScript possible.
So, we created an entry for each significant page and loaded only what that page
needed. Nowadays, thanks to the massive compression of web assets, this is not
worth it for normal pages. So, we’ll remove some entrypoints in the future and
leave only the entrypoints of very complex pages.

Changes in Asset Files

Your files inside the assets/ directory don’t require many changes, but you
might need to do some tweaks. Now, every time you import files, you must include
the file extension:

// assets/some-file.js
– import ‚./bootstrap‘;
– import ‚./code‘;
+ import ‚./bootstrap.js‘;
+ import ‚./code.js‘;

Another tweak we made is in the way we load the Bootstrap CSS styles. Before,
we used to pick and choose which parts of Bootstrap to use:

// assets/styles/app.scss
@import „~bootstrap/scss/root“;
@import „~bootstrap/scss/reboot“;
@import „~bootstrap/scss/utilities“;
@import „~bootstrap/scss/helpers/visually-hidden“;
@import „~bootstrap/scss/images“;
@import „~bootstrap/scss/breadcrumb“;
// …

We did this to optimize assets as much as possible and only include the parts
that we’re going to use. Now we include the entire Bootstrap CSS styles:

// assets/app.js
import ‚bootstrap/dist/css/bootstrap.min.css‘;
// …

This is an acceptable trade-off for us because we use most of the Bootstrap CSS
styles and because, thanks to compression, browsers only need to download 34 KB
to get the full Bootstrap styles.

Changes in Templates

Our templates followed a very typical hierarchy and loaded Webpack Encore entries
like this:

{# templates/base.html.twig #}
{% block stylesheets %}
{{ encore_entry_link_tags(‚app‘) }}
{% endblock %}

{% block javascripts %}
{{ encore_entry_script_tags(‚app‘) }}
{% endblock %}

{# templates/conference/schedule.html.twig #}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags(’schedule‘) }}
{% endblock %}

{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags(’schedule‘) }}
{% endblock %}

All pages need the app.css and app.js assets, so we load the app
entry in the base.html.twig template. Then, if any template needs additional
assets, they load other Webpack Encore entries using Twig template inheritance.

Now, the same templates look like this:

{# templates/base.html.twig #}
{% block importmap %}
{{ importmap(‚app‘) }}
{% endblock %}

{# templates/conference/schedule.html.twig #}
{% block importmap %}
{{ importmap([‚app‘, ’schedule‘]) }}
{% endblock %}

Now it looks much simpler, because you only need to importmap() one entrypoint
to get both the CSS and JavaScript files. However, there’s a drawback. You can’t
use template inheritance to add more entrypoints to the page. You can only call
importmap() once per page. So, the base entrypoints used by all pages (e.g.
app) must be repeated in all the importmap() calls of the templates
that add new entrypoints.

Saas and CSS

We used Sass in all the applications instead of pure CSS. Sass served us well
during these years, but we no longer need it. The only two Sass features that we
used were:

Variables: no longer needed thanks to CSS custom properties;
Nested selectors: no longer needed thanks to CSS nesting.

In most websites, we refactored the Sass files into pure CSS files. Given that
CSS nesting is still at 80% browser support, we removed nested selectors and
we’ll add them back again when support is more widespread.

In another website, we kept the .scss files because moving to CSS was too
much work. In that project, we installed the symfonycasts/sass-bundle which
makes it easy to use Sass with Symfony’s AssetMapper Component (it doesn’t require
using Node.js).

Tailwind CSS

In some website, we use Tailwind CSS instead of Bootstrap. Luckily, there’s
a symfonycasts/tailwind-bundle that takes care of everything for you. This
bundle doesn’t require Node.js either and it’s based on a binary file that
compiles the Tailwind CSS styles.

Changes in CI and Deployment

This was one of the easiest parts of the upgrade process. You only need to update
the commands run to build/compile the assets:

// .github/workflows/tests.yaml
– name: Build and compile assets
run: |
– npm install
– npx encore production
+ php bin/console importmap:install
+ php bin/console sass:build
+ php bin/console asset-map:compile

The deployment process required zero changes. Thanks to its tight integration
with Symfony, Platform.sh already detects if you are using AssetMapper, SassBundle
and/or TailwindBundle and runs the required commands for you.

Conclusion

We’re extremely happy with the upgrade to AssetMapper. It feels like cheating
because you end up with the same results you’d get with Webpack Encore, yet it
requires only a fraction of the effort.

Thanks to the removal of files like package.json, the Pull Requests resulted
in the removal of a lot of lines of code:

live.symfony.com: +981 lines added, -9,971 lines removed
certification.symfony.com: +718 added, −10,519 removed
symfony.com: +1,105 added, −10,088 removed

And these stats don’t include the removal of the node_modules/ directory,
which will save you a ton of disk space.

Another nice improvement is that changes in assets are now propagated instantaneously
to the browser: make a change, save files, reload page. No more waiting to asset building;
except if you use Sass or Tailwind CSS, in which case you need to run sass:build –watch
or tailwind:build –watch to rebuild assets when they change.

Finally, AssetMapper keeps the great performance of the websites. Even if we now
serve many more CSS/JavaScript assets to the browsers, the performance score is
close to 100/100 in most pages.

AssetMapper is the present and future of asset management in Symfony applications.
If you can convince your boss/client to get the resources needed to upgrade your
own projects to AssetMapper, don’t think twice and upgrade as soon as possible.

Sponsor the Symfony project.

Symfony Blog

Read More

Latest News

PHP-Releases

PHP 8.3.9 released!

PHP 8.2.21 released!

PHP 8.1.29 released!

Generated by Feedzy