Need help with Drupal or Grav admin, site building, design, or content? Contact us!   

Back to top with progress scrollbar

Join us as we explore the strange new world of Symfony’s AssetMapper via SymfonyCast

By Reuben Walker, 2 August, 2023
Illustration of earth with nodes surrounding it

This article could also be titled AssetMapper: Modern JS without BS (either Build System or Bullshit). But we want to boldly go where no one has gone before in the Symfony universe. 🛸

In any event, it will explore Symfony’s new and currently experimental AssetMapper component.

While my JavaScript skills are currently better than my Symfony ones, I am by no means an expert. Coding boot camp only gets a content creator so far. Plus, as you just read, AssetMapper is at the experimental stage and too new for anyone to have developed expertise by using it.

So, I am mainly going to paraphrase/plagiarise/quote and summarize general points from the SymfonyCasts course - AssetMapper: Modern JS with Zero Build System. All credit for the outstanding content goes to the esteemed Ryan Weaver. At least I won’t use any mofoing “AI”.

I will also emphasize the things it does to make your Symfony apps even more outstanding. Like being best friends with Symfony UX and its Stimulus and Turbo goodies.

Ryan is my favorite Symfony developer. And it’s because I’m a frontend guy and he’s contributed fantastic things to the frontend aspects of Symfony. Not because he’s super, super awesome. And funny. And intelligent. And handsome.

So please don’t sue me Ryan. 😜

I will supplement as needed from the official documentation.

To take the course you have to and should subscribe to SymfonyCasts. It is an outstanding value. I know because I obviously use it personally. Send some love and cash Ryan’s way.

And if you want to see how all this works from a technical standpoint, you will need to watch the AssetMapper SymfonyCast after subscribing. You will also need it if you want to make sense of the directories and files mentioned in this article.

You can check out the official Symfony docs for the AssetMapper component for more details.

My opinions and comments will be in bold. And believe me, I have them.

Engage Symfony Trekkers.

JavaScript Build Systems

JavaScript build systems were created because browsers didn't support modern ES6 features. Ones like the import statement, const, the class syntax, etc. If you tried to run this kind of JavaScript in a browser, you would have been greeted with error messages.

So, a JS build system was needed to transpile (convert) new-looking JavaScript to old-looking JavaScript to run in the browser. It would also combine JavaScript and CSS files so that we would have fewer requests, and it could create versioned filenames, process TypeScript and JSX, Sass, and more.

These systems are super powerful. But they also add complexity and slow down coding.

Note: I experienced all these horrors in coding boot camp.

With AssetMapper you can write the modern JavaScript that you know and love but with no build system, and no Node JS. Fuck yeah.

It’s just you and the browser: the way the Gods of the Internet intended it.

And your Symfony app is going to be as performant and fast as one built with a build system.

And if you're wondering about things like Sass preprocessors, or Tailwind, you can use those if you are lazy.

Note: Please do not use Failwind in your projects. Learn CSS peeps. It’s fucking easier in the long run without any horseshit hacks. Bootcrap is almost as bad.

Modern JavaScript

Much of modern JS was introduced in ES6, or ECMAScript 6. You'll hear ES6 a lot because most modern features you're used to came from this version - released back in 2015.

Unfortunately, this was the kind of code that browsers historically choked on!

So, usually, we would need a build system like Symfony Encore that would read this modern code and rewrite it to old JavaScript so it would work in our browser.

But that’s the past. Today, ES6 and newer versions work in our browser.

Note that if you want your JavaScript to be able to use import and export, you need to load the original file "as a module" in your Twig templates.

You can also import third-party packages like lodash.

However, using import can cause a variety of problems (versioning, caching, etc.) Problems that Symfony's new AssetMapper component will help us solve.

Now’s a great time to subscribe to SymfonyCasts. 😉


For now, AssetMapper is experimental in Symfony 6.3, so there will likely be backwards compatibility breaks before 6.4. But, the concepts in the SymfonyCast are solid, and you can deploy a super-performant site with AssetMapper today.

The Symfony Flex recipe for installing AssetMapper adds several things to your app. First, it gives you an assets/ directory... which looks pretty much identical to what you would get if you installed WebpackEncore. You have an app.js file - this will be the main, one file that's executed - and also app.css, the main CSS file.

In templates/base.html.twig, the recipe also adds a link tag to point to app.css.


AssetMapper is really quite simple. It has two main things (and they’re not deuterium and anti-deuterium):

  • Mapping & Versioning Assets - All files inside of assets/ are made available publicly and versioned.
  • Importmaps - A native browser feature that makes it easier to use the JavaScript import statement without a build system.

AssetMapper configures "paths" - like the assets/ directory - and it makes the files inside available publicly.

Because we've pointed AssetMapper at the assets/ directory, we can refer to things inside of that via their path relative to that root. This is known as the "logical path" to the asset.

Symfony bundles can add also their own AssetMapper paths to make their files publicly available.

Again, the AssetMapper component works by defining directories/paths of assets that you want to expose publicly. These assets are then versioned and easy to reference.


When we're talking about the frontend of a site, we're mostly talking about two things, CSS and JavaScript (in addition to the HTML handled by Twig templates).

The CSS side of things is dead simple with AssetMapper. You create a CSS file inside the assets/ directory then include it with a good old-fashioned link tag in your base.twig file that uses the CSS file's logical path. That's it. Zero magic.

The point is: you get to code CSS like normal and everything just works.

3rd Party CSS

In AssetMapper, because there's no Node, we don't have an easy system for grabbing CSS packages. But we can still get them via a CDN.

If you want to avoid using the CDN, you can download the package file directly into your project. Again, because there's no package system like NPM, so create an assets/vendor/ directory and put the file inside of that. Then commit that assets/vendor/ directory to Git to keep it in your project and versioned. Committing vendor files into your project isn't amazing, but it's not a huge deal and is your best option right now if you want to avoid a CDN.

So, you can use 3rd party CSS, but don’t. Because its 90% horseshit.

I will allow that custom and variable fonts are in the 10%. Although system fonts are always more performant.

You can use Tailwind but it requires a build step. But, fuck Failwind anyway, because it’s about 65% of the 90% BS above. Don’t be a lazy programmer. Go vanilla or go home. Or hire a frontend developer.

But, let’s get off the soapbox and more on to JavaScript.

Importing JS

Imported JavaScript is generated from an importmap.php file inside your Symfony project. The file isn't super-interesting but it'll be more useful with third party JavaScript. It has an app key that points to our assets/app.js file using its logical path.

Thanks to that, this <script type="importmap"> dumps onto the page. When you import something that doesn't start with a ".", "/", or "../", that's called a bare import. You will usually see this for third-party libraries.

In the browser environment, when it sees a "bare import", your browser looks for an importmap on the page to find a matching entry.

The browser sees import 'app', finds the key, and that's the path it downloads. It effectively copies this path here and pastes it down there. That's why our app.js file is being executed: it's team work between the importmap and the extra <script type="module"> that bootstraps your app.

The best thing about importmap is that it's not a Symfony thing: it's just an internet thing. It's how your browser works. We have this importmap.php file, which is a Symfony thing. But once its is on the page, your browser is doing the work.

And importmap works in most browsers. About 81% of browsers at this time. That could be a problem, except that the importmap() function also dumps a shim.

Thanks to the shim, if a browser does not support importmap, it adds that functionality. So, it just works.

Thanks to the {{ importmap() }} Twig function, the assets/app.js file is loaded and executed by the browser.

The importmap is constructed from two sources. The first source is the importmap.php file. The second source is more subtle. Whenever our JavaScript imports another JavaScript file using a relative path, that imported file is automatically added.

This is powerful. It means that our final code can look like it originally does: ./lib/vinyl.js. But thanks to the importmap, our browser will smartly download the real file with the long version part in the name.

3rd Party JS

IMHO with AssetMapper we're thankfully not using yarn or npm, but we can achieve the same functionality.

Over in your terminal, open a new tab, and run php bin/console importmap:require followed by the name of the NPM package you want: for example lodash:

php bin/console importmap:require lodash

It adds lodash to importmap.php and tells us we can use the package as usual. This means we can say import _ from 'lodash' and everything will work.

How? When we ran the command, it made one change: it added a section to importmap.php. And as hip as this is, it's not magic. Behind the scenes, the command went to the JSDelivr CDN, found the latest version of lodash, then added the lodash key set to that URL.

When our browser sees import _ from 'lodash', it looks inside the importmap for lodash, finds this URL, and downloads it from there.

But, if you don't want to rely on the CDN, you don't have to. To avoid it, when you require the package - or any time later - pass the --download option:php bin/console importmap:require lodash --download.

If we ran code through Symfony Encore, Encore would do something called "tree shaking". This is where it would see that we're only importing camelCase from lodash. And so, in the final JavaScript, it would only give us the code for camelCase, not the complete lodash package.

In a browser environment, if you import from lodash, you're going to get all of lodash even if you're only importing one part of it. Now, that might not be that big of a deal. Thecomplete build of lodash is still only 24 kilobytes. But what if you were using a big package but only need to import one specific thing?

Often, there's a specific file that we can import, like /camelCase. You'll usually find details about these files in the package documentation.

Documentation is your friend.

Stimulus Components (text)


In a Symfony project’s UI, components are handled by a group of tools with the moniker of Symfony UX.

And it is one of the greatest things in Symfony. It’s 10,000% a better option than importing 3rd party JS.

Symfony describes Symfony UX as “JavaScript tools you can't live without. They’re a set of PHP and JavaScript packages to solve everyday frontend problems featuring Stimulus and Turbo.”

“Symfony UX is an initiative and set of libraries to seamlessly integrate JavaScript tools into your application.

Behind the scenes, the UX packages leverage Stimulus: a small but powerful library for binding JavaScript functionality to elements on your page.”

Thank you, abstraction.

StimulusBundle is a relatively new package that houses some Twig shortcuts that you can use, like stimulus_controller(). But, more deliciously, it has a recipe that will set your app up to load Stimulus controllers effortlessly.

Technically, Stimulus and AssetMapper are best friends. bootstrap.js loads a file that starts Stimulus and that automatically loads everything inside the assets/controllers/ directory as well as any 3rd party UX packages in assets/controllers.json.

Another part of StimulusBundle is the ability to get more Stimulus controllers by installing a UX package. For example Turbo.

There is a common pattern with UX packages: if a UX package depends on a third-party package, its recipe will add that package to your importmap automatically. The result is that, when that package is referenced - like import '@hotwired/turbo' - it just works.

When you install StimulusBundle, its recipe comes bearing gifts - one of which is ux_controller_link_tags(). Some UX packages come with CSS files. You'll find them under a key called autoimport, which the recipe will add under the controller. This ux_controller_link_tags() finds all the CSS files for all the controllers you have activated, and it outputs them.

You can also use lazy Stimulus controllers to keep your initial page lightweight if you have some heavy Stimulus controllers that are only used on certain pages.

Page-Specific CSS & JS

Suppose you have some CSS and JS that are only needed in certain areas like admin for instance. If you write that in the normal way and in the normal files, that code is going to be downloaded everywhere, including the frontend of our site. That, at the very least, is wasteful. A better way is to only download the admin CSS and JS when you visit the admin area.

You can do it with lazy Stimulus controllers. But another option is to create an extra set of CSS and JavaScript that are explicitly loaded only on the admin pages via AssetMapper.

Let's start with CSS, which is simple. In the assets/styles/ directory, create an admin.css file. And add the CSS code you need. At this point, the new admin.css file is technically available publicly because it's in the assets/ directory. But, we're not using it yet. To do that, we need a link tag to the corresponding Twig template file.

But what about JavaScript? Create a new file maybe next to app.js called admin.js. Like with the CSS file, this file is now publicly available but nothing is loading it. In dashboard.html.twig, say {% block javascripts %}, {% endblock%}, then {{ parent() }}. Below that, add a <script> tag with type="module". Now we're going to code as if we're in a JavaScript file. Say import and then the path **to the JavaScript file. To get the real path we use the asset() function and pass the logical path: admin.js. You’re good to go.

One of the things you’ve seen is that everything in the assets/ directory is exposed publicly which is the whole point of AssetMapper!

You can also exclude files with AssetMapper.

It’s still a great time to subscribe to SymfonyCasts. 😉

Hint: host your Symfony apps on

You can deploy your code where ever you want and use any service or web server. It doesn't matter with AssetMapper. The only requirement is that your web server supports HTTP/2 so that your assets - the JavaScript and CSS files - can be downloaded in parallel super fast.

So, if you want to deploy to, watch their chapters on the Asset Mapper SymfonyCast.

Compiling Assets for Production

Once your production environment is set up, how do you get your assets onto the site? If you "View Page Source", it appears things are working. We see the importmap and these paths look correct: they even have the version part in their filenames.

Unfortunately, all of these files return a 404. In the dev environment, when we're working locally, these files don't physically exist. But an internal Symfony listener intercepts the request, finds the file, and serves them.

But in the prod environment, that system isn't even active. It's too slow to run on production... so everything just 404s. And that's okay! A long time ago, we learned about the command to fix this:

php bin/console asset-map:compile

This command's job is simple: it takes all the assets that AssetMapper knows about and moves them into the public/assets/ directory. It's not a command you need to run locally, but it is something you need to run when you deploy.

Watch the course for the rest of the technical details.

Once you're on production, you're ready to make sure your assets are served super fast.

Long-Term Caching, Compression, and File Combining

There are a few things we need to check on, to make sure our site is fast.

1) HTTP/2

First check that your web server is using HTTP/2.

2) Combining Files

The second thing is that nothing in AssetMapper ever combined our files to reduce the number of HTTP requests. Thanks to HTTP/2, you almost definitely do not need to combine your files together. So if you were thinking that this was missing, it's not! It's by design.

3) File Compression / Minification

But what about minifying your files? It's true:currently, our files are being served without minification, which is a problem. We want our CSS and JavaScript files to be minified or at least compressed. And this is item number three to think about.

But... this is something that can be done by our web server. Yup, if you ask kindly, you should be able to convince your web server to compress your files so they're smaller when being sent across the network.

4) Long-Term File Caching

The fourth and final thing we need to do is ensure that all of our static files are set up for long-term expiration. Because we have these nice versioned file names, when a user visits our site, we want them to download this file once and never again. We want them to cache it forever. Because if we change anything inside this file, the whole filename will change. And the user's browser will naturally download the new version the next time they visit the site.


You should use preloading. For all the details, watch the final chapter in the course. But as regards AssetMapper:

You need a Symfony component called "WebLink". At your terminal, run:

composer require symfony/web-link

Once that's done, back in base.html.twig, add another preload down here that looks similar: <link rel="preload" href="">. This time, use a Twig function called preload() passing the normal asset() function to point to styles/app.tailwind.css. In this case, this preload function needs another option called as: 'style'.

It turns out that the preload() function does two things: it outputs the link tag href... but it also tells Symfony that it should add a preload header to the response.

As the browser starts downloading the response, at the very top it sees a hint that it should start downloading that CSS file!

For JavaScript preloading, the importmap() Twig function dumps the importmap and the <script type="module">. But it also dumps modulepreload things. These are cool. Because we said 'preload' => true for app, it adds a <link rel="modulepreload"> for app.js. That hints to the browser that it should start downloading app.js immediately.

The real power is that AssetMapper then sees that assets/app.js imports bootstrap.js. And because app.js is preloaded, it also preloads bootstrap.js. And since this imports ./lib/vinyl.js, it also preloads ./lib/vinyl.js. So it will download all three of these files immediately.

Ryan finishes the course with this:

I think AssetMapper is a breath of fresh air - and I hope you feel the same! There are some things it doesn't do, like tree-shaking or handling TypeScript. But for a large number of projects, it's a great fit! And the cool thing is, you're still writing normal JavaScript. So if you ever did need to move to a build system later, you could do that.

Wrapping Up

Thanks for reading. You’ve explored how AssetMapper gives your Symfony app modern JS without a build system or bullshit. And it works beautifully with Symfony UX's StimulusBundle.

When AssetMapper is finalized in Symfony 6.4, be sure to put it to use. It will go a long way toward taking the BS in JS out of your applications.

And be sure to subscribe to SymfonyCasts. Send Ryan some love and cash. And thank him for the AssetMapper course. Again, I don’t want to get sued. 😉

Then take the course. You'll get a nice certificate.

Live long and prosper. 🖖

section separator
Article Type
Donate using Liberapay

Symfony Station covers the essential news in the Symfony, PHP, and Fediverse development communities with a focus on protecting democracy. Please use the button above to make a small donation to help cover our out-of-pocket costs. Our labor is provided free of charge to support the communities we write about.

Join our newsletter list

Subscribe to The Payload, our weekly newsletter exploring the Symfony Universe.

  Start exploring!

Please share