I deleted webpack.config.js from the internal admin on 2 October. In February last year I wrote that the Encore config was still in that repo and still in the Dockerfile because of Sass, and I left it there for twenty months. What finally moved it was not AssetMapper learning to compile Sass. It was me accepting that Sass is somebody else’s bundle now.
So we currently run three front-end setups across the applications I touch, and I have stopped apologising for that. The internal admin runs AssetMapper with no Node on the build agent at all. A client dashboard runs Vite behind pentatrion/vite-bundle. One older application still runs Encore, and it will keep running Encore. Those are three different front ends, not three moods.
Encore did not die, it stopped being the default
Worth getting this right, because I have read three posts this year that declared Encore abandoned. It is not. The front-end page in the 7.3 docs lists AssetMapper and Encore side by side, and the comparison table marks both as production ready and both as stable. The last Encore release, 5.2.0, went out on 25 August 2025. That is a maintained package by any definition I use.
What changed is the default. The same page says that new webapp projects created with symfony new --webapp use AssetMapper, and then gives you the command sequence to remove it and put Encore back if that is what you want. Reading the order of that page tells you the whole story: AssetMapper is described as the recommended system, Encore is described second, and switching to Encore is documented as a deviation you perform deliberately.
The thing that costs money is not which one is recommended. It is that the two have different failure modes, and the table in the docs is honest about them. AssetMapper needs nothing installed. Encore needs Node. AssetMapper always versions assets; Encore makes versioning optional. AssetMapper does not remove comments from your JavaScript or CSS, and Encore does. Every row in that table has shown up in a real decision for me at some point.
What AssetMapper actually does
Two features, and it helps to say them plainly. Everything under assets/ is exposed publicly with a version hash in the filename, so {{ asset('images/duck.png') }} renders as /assets/images/duck-3c16d92m.png. And an importmap is written into the page, so a bare import { Alert } from 'bootstrap' in your own file resolves in the browser without a bundler having rewritten anything.
Third-party packages go through one command. php bin/console importmap:require tom-select adds an entry to importmap.php and downloads the files into assets/vendor/:
// importmap.php
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'tom-select' => [
'version' => '2.4.3',
],
];
The part that caught me is that assets/vendor/ is gitignored by the Flex recipe, and the files come from the jsDelivr CDN. So the deploy has a network dependency it did not have before, and the deploy script grew a line:
php bin/console importmap:install
php bin/console asset-map:compile
On the admin, asset-map:compile writes 1,240 files into public/assets/ in about 2.4 seconds on the build agent. The Encore build it replaced took 68 seconds and needed a node_modules tree I measured at 340 MB before I stopped measuring it. Our CI minutes for that project dropped by roughly a third, most of which was the npm install step rather than the build. There is also a debugging command I use more than I expected, php bin/console debug:asset-map, which prints every logical path and the file it maps to. When an asset 404s, that command answers the question in one shot.
Where AssetMapper stops
It compiles nothing. That is the design, not a gap, and it is exactly where the arguments start.
Sass is a separate bundle, symfonycasts/sass-bundle, which downloads the correct Sass binary itself and gives you a sass:build command. No Node, which is the only reason I finally did the migration: our objection to AssetMapper in 2024 was that moving to it meant keeping Node around anyway for stylesheets, and the bundle removes that objection. TypeScript is a different separate bundle, which the docs point you at. Minification is a third one, sensiolabs/minify-bundle, because AssetMapper on its own leaves your comments in the served file and relies on the web server compressing the response.
Then the hard limit. The docs answer the question “Can I Use it with JSX or Vue?” with “Probably not”, and the detail underneath is the part to read twice: JSX can be compiled to a plain JavaScript file, but if you are writing a lot of JSX you want a real bundler, and Vue single-file components cannot work at all because a .vue file has to be compiled by a build system. Our dashboard is about ninety single-file components. That is the end of the conversation for that application, and it was a two-minute conversation.
The dashboard that kept a bundler, and got Vite
The dashboard used to be an Encore build with enableVueLoader(). It now runs Vite 7, which landed on 24 June 2025, wired up with pentatrion/vite-bundle 8.2.2 and the matching vite-plugin-symfony 8.2.2. The bundle is a subtree split of the symfony-vite-dev repository, so issues and examples live in the parent repo, which confused me for about an hour when I went looking for the changelog.
The mechanism is small and I like it for that. The plugin writes an entrypoints.json next to the build output, and the bundle reads it from PHP and renders the tags. In dev the file points at the Vite server; after a build it points at hashed files. The installation docs give you two Twig functions and nothing else to remember:
{% block stylesheets %}
{{ vite_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ vite_entry_script_tags('app') }}
{% endblock %}
// vite.config.js
import { defineConfig } from "vite";
import symfonyPlugin from "vite-plugin-symfony";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [
vue(),
symfonyPlugin(),
],
build: {
rollupOptions: {
input: {
app: "./assets/app.js",
},
},
},
});
And the bundle config, which is four lines because the defaults are the ones you want:
# config/packages/pentatrion_vite.yaml
pentatrion_vite:
build_directory: build
# our containers cannot reach the host on localhost
proxy_origin: 'http://host.docker.internal:5173'
throw_on_missing_entry: true
preload: link-header
Two of those are opinions rather than defaults. throw_on_missing_entry defaults to false, which means a typo in an entry name renders nothing and you stare at an empty page; turning it on turns that into an exception. And preload defaults to link-tag, which emits modulepreload tags; link-header sends HTTP/2 Link headers instead and needs symfony/web-link installed. The proxy_origin line exists purely because we develop in containers and the default origin cannot be reached from inside one.
What we bought is hot module replacement. Editing a Vue component and seeing the state survive the update is worth real money on a dashboard with filter panels four levels deep, and Encore’s dev server never got close to that feel for us. Cold npm run build on that project is 11 seconds against 54 with Encore. The dev server starts in under a second, which is the number that actually changed how the front-end work on that project feels day to day.
The rule I would give someone else
| Front end | Tool | Reason |
|---|---|---|
| Twig pages, Stimulus, a few hundred lines of JS | AssetMapper | No Node, no build step to break, versioning you get for free |
| Twig pages plus Sass and TypeScript | AssetMapper plus the two bundles | Still no Node; two extra composer packages beats a bundler config |
| A component tree, single-file components, JSX | Vite | Hot module replacement and a compiler you are going to need anyway |
| An existing Encore build that nobody is complaining about | Encore | It is stable, released, and migration buys you nothing here |
The one I would argue about is the middle row. If you find yourself installing a third bundle to get back a thing a bundler does by default, you have probably crossed over and should use Vite. Two bundles is fine. Four is a config you will not recognise in a year.
Still unresolved on my side: third-party CSS through the importmap works by way of a small JavaScript shim that appends a link tag at runtime, and I do not love that our stylesheet loading order now depends on module evaluation order. And I have not solved the jsDelivr dependency at deploy time properly. Right now a CDN outage means a failed deploy, and my plan of committing assets/vendor/ against the recipe’s advice is a plan I have not tested yet.
Sources
- Front-end tools, Symfony 7.3 docs: AssetMapper recommended for new projects, the AssetMapper versus Encore comparison table, and the documented steps to switch a webapp project back to Encore.
- AssetMapper documentation:
importmap.phpshape,importmap:require,importmap:install,asset-map:compile,debug:asset-map, the gitignoredassets/vendor/directory, and the “Probably not” answer on JSX and Vue single-file components. - Webpack Encore 5.2.0: the release that shows Encore was still shipping in August 2025.
- symfonycasts/sass-bundle: downloads the Sass binary with no Node required and adds the
sass:buildcommand. - pentatrion/vite-bundle: the two Twig functions, and the note that the package is a read-only subtree split of
symfony-vite-dev. - Symfony and Vite installation guide: the Flex recipe, the Twig functions in a base layout, and the dev server on port 5173.
- vite-bundle configuration tree, v8.2.2: the real defaults behind
build_directory,proxy_origin,throw_on_missing_entryandpreload. - Vite 7.0 announcement: release date of 24 June 2025.