Vite for Symfony: Replacing Webpack Encore Without Breaking Your Asset Pipeline
Webpack Encore had a long run as the default asset pipeline in Symfony projects, and it still works. The Symfony team maintains it, your builds pass, nothing is on fire. But the ecosystem around it has moved. New Symfony 7 projects start with AssetMapper or Vite, webpack itself sees little active development, and most frontend tooling released since 2024 assumes native ES modules. If your application still boots Encore on every build, a Webpack Encore to Vite migration is worth planning, and it is less disruptive than most teams expect. This post covers what actually changes, the three migration approaches we use in client projects, the Encore settings that have no direct Vite equivalent, and the build times we measured before and after a real migration.
What you gain, concretely
Vite splits development and production into two separate pipelines. In development it serves your source files as native ES modules and transforms only what the browser requests, so there is no upfront bundling at all. The dev server starts in well under a second even on large projects, and hot module replacement applies a CSS or component change almost instantly because Vite only re-transforms the file you touched. Production builds go through Rollup and produce hashed, tree-shaken bundles, much like Encore does today.
For a Symfony team the differences that matter day to day are dev server startup time, HMR speed that stays flat as the project grows, and a configuration file that is a fraction of the size. Most of what webpack.config.js wires up by hand, loaders, dev middleware, code splitting, is default behavior in Vite.
Be honest with your team about what you do not gain. Vite skips type checking entirely during transforms, so slow TypeScript feedback stays slow unless you run tsc separately, which you should be doing in CI anyway. And production build times improve, but far less dramatically than the development feedback loop does.
The two packages that replace Encore
On the Symfony side, symfony/webpack-encore-bundle gets replaced by a pair:
- vite-plugin-symfony, an npm package that configures Vite for Symfony's directory layout and writes an entrypoints.json the Twig side can read
- pentatrion/vite-bundle, a composer package providing vite_entry_script_tags() and vite_entry_link_tags(), the direct counterparts of encore_entry_script_tags() and encore_entry_link_tags()
A minimal vite.config.js for a Symfony project looks like this:
import { defineConfig } from 'vite';
import symfonyPlugin from 'vite-plugin-symfony';
export default defineConfig({
plugins: [symfonyPlugin()],
build: {
rollupOptions: {
input: {
app: './assets/app.js',
admin: './assets/admin.js',
},
},
},
});
Compare that to a typical webpack.config.js with Encore chained calls for entries, Sass, PostCSS, source maps, versioning, and runtime config, and the reduction is obvious. Entries map one to one: every Encore.addEntry() becomes a key under rollupOptions.input.
In Twig, the change is mechanical:
{# before #}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }}
{# after #}
{{ vite_entry_link_tags('app') }}
{{ vite_entry_script_tags('app') }}
If you use Stimulus and Symfony UX, vite-plugin-symfony ships a Stimulus bridge, so controllers keep working. Budget an hour to verify lazy controllers, since their loading behavior depends on the bundler.
Three approaches to a Webpack Encore to Vite migration
Which path fits depends on how much frontend code you have and how much risk the team can absorb in one release.
The first approach is full replacement in a single pull request. Remove Encore, install both packages above, port the config, swap the Twig helpers, delete webpack.config.js. For applications with a handful of entry points and a conventional setup (Sass, PostCSS, some TypeScript), this is one or two days of work including testing. It is the right call for most projects because running two bundlers has its own carrying cost.
The second approach is a hybrid with AssetMapper. Symfony's AssetMapper serves simple assets without any build step: it maps files, versions them, and writes an importmap. If large parts of your asset directory are plain CSS and vanilla JS that only pass through Encore because everything had to, move those to AssetMapper and reserve Vite for the code that needs compilation, such as TypeScript, JSX, or Sass. You end up with two small, purpose-matched pipelines instead of one large one. The cost is that your team needs to know which asset lives where, so document the split in the repo.
The third approach is incremental, one entry point at a time. Encore and the Vite bundle can coexist: both write into public/build under different subdirectories, and each Twig layout calls the helper that matches its entry. Migrate the admin bundle this sprint, the checkout flow next sprint, and keep shipping features throughout. This is the safest route for large applications with many entry points and heavy webpack customization. Set a deadline for finishing, though. Every week both bundlers exist, CI runs both builds and every dependency upgrade has to satisfy two toolchains.
The Encore settings with no direct equivalent
Most of the migration is renaming. These four areas are the ones that need actual thought.
Encore's copyFiles() has no counterpart in Vite's config. Vite expects static files to either live in a public directory that gets copied verbatim, or to be imported from source files so they enter the dependency graph and get hashed. For most projects the fix is moving copied files (icons, legacy vendor scripts, robots exclusions) into the public directory. If you need transformed copies, rollup-plugin-copy fills the gap.
Environment variable injection works on an allowlist. Encore setups often use configureDefinePlugin() to inline arbitrary values at build time. Vite only exposes variables prefixed with VITE_ through import.meta.env, and everything else is invisible to client code. This is a better default, because it makes it harder to leak a server-side secret into a bundle, but it means you must rename the variables your frontend actually reads and audit what was being inlined before. In two of the last three migrations we did, that audit surfaced values in the bundle that should never have been there. Treat the audit as a feature of the migration, not a chore.
autoProvidejQuery() is gone, and there is no polite replacement. Vite will not silently hand a global $ to every module. Either add explicit jquery imports in the files that use it, which a codemod handles quickly, or wire @rollup/plugin-inject to reproduce the old behavior. Take the explicit imports if you can. The magic global is exactly the kind of hidden coupling that makes the next migration harder.
PostCSS, on the other hand, moves without changes. Vite reads postcss.config.js natively, so Tailwind, autoprefixer, and friends keep working the moment the file exists. Fonts and images referenced from CSS are also handled automatically, including hashing.
The CI change
The deployment story barely changes, which is the point of using the bundle pair. Your CI step goes from running Encore's production build to running vite build. Output still lands in public/build, filenames are still content-hashed, and the entrypoints file still tells Twig what to include, so your web server config, CDN setup, and deploy scripts stay as they are. The one thing to double check is your CI cache configuration: Vite's dependency pre-bundling cache lives in node_modules/.vite, and caching it shaves the install-and-build step further.
The numbers from a real migration
We migrated a mid-size B2B Symfony application this spring: six entry points, TypeScript plus Sass, Stimulus controllers, around 900 modules in the graph. Same CI runner before and after. The production build went from 84 seconds with Encore to 31 seconds with Vite. The dev server, which took about 20 seconds to become usable under webpack, now starts in under a second. HMR went from one to three seconds per change to effectively instant. No visual regressions surfaced in testing; the only runtime issue was a legacy script that relied on the injected jQuery global, caught in staging.
The team's own summary after two weeks was that nobody wanted to touch a webpack project again. Faster feedback compounds: developers reload less, batch changes less, and trust the dev environment more.
When staying on Encore is defensible
Migrating build tooling is real work, and not every project should do it now. If the application is in maintenance mode and sees a frontend change a quarter, Encore will keep building it for years. If your setup leans hard on webpack-specific plugins with no Rollup equivalent, price in the effort of replacing them before you commit. And if your frontend is simple enough to need no build step at all, skipping Vite and going straight to AssetMapper removes the Node toolchain from the project entirely, which is an even better outcome.
What we advise against is deciding by inertia. Encore's dependency tree ages, webpack 5 gets patches but little more, and every year the migration waits, the surrounding ecosystem drifts further toward ES modules. Asset pipelines are the kind of infrastructure that is cheap to modernize on your schedule and expensive to modernize under pressure.
If you are weighing this migration as part of a broader modernization effort, that is work we do regularly, from legacy stack assessments to tech stack decisions for teams planning the next three years. Write to hello@wolf-tech.io or have a look around wolf-tech.io and we can tell you fairly quickly whether a one-PR replacement or an incremental path fits your codebase.

