Rikki BochowFront End Developer

In a previous post, Jack walked through our frontend tooling setup using Vite, Storybook and ESLint. Since then, we've been iterating on that stack.
We've made some upgrades since, so here's why we made them and the performance gains we saw.
The most significant upgrade was moving from Vite 7 to Vite 8. The upgrade itself was relatively straightforward, with one notable gotcha: Vite 8 defaults to Lightning CSS for CSS minification, which mangled some of our CSS out of the box. The fix meant explicitly excluding the features causing issues:
import { Features } from 'lightningcss'
export default defineConfig(() => ({
css: {
lightningcss: {
exclude: Features.LightDark | Features.LogicalProperties | Features.Nesting,
},
},
}))
This also meant we could drop postcss-preset-env entirely, since Lightning CSS handles the transforms natively. We kept only postcss-custom-media for custom media query support.
Jack's config used browserslist-to-esbuild to pass a browser target to Vite. Vite 8 ships with built-in support for the Baseline metric, which gives us a simpler, standards-based way to define our browser target without an extra dependency:
export default defineConfig(() => ({
build: {
target: 'baseline-widely-available',
},
}))
This allowed us to remove both the browserslist and browserslist-to-esbuild packages and the .browserslistrc file entirely. In fact, this config isn't actually needed since it's the default target in Vite 8.
The original config used tinyglobby to glob for entry files. Node.js now ships globSync as part of the node:fs module, so we were able to drop the dependency entirely:
import { globSync } from 'node:fs'
const entry = globSync(['**/*.entry.js', '**/*.css'], {
ignore: [
'**/_*.css',
'node_modules',
// ...
],
})
One less package to keep updated.
The most impactful change for day-to-day development was swapping ESLint and Prettier for their Rust-based counterparts from the oxc project. Both tools claim raw speedups of 50-100x for individual file operations. Across our full lint, format and build pipeline, we saw a more modest 1.9x to 2.8x, which is still a meaningful chunk of a developer's day.
oxlint is a drop-in replacement for ESLint written in Rust. Migration was painless thanks to the @oxlint/migrate tool, which reads your existing ESLint flat config and generates an .oxlintrc.json automatically:
npx @oxlint/migrate
It handles rule mapping, globals, ignore patterns, and plugins. If any of your ESLint rules aren't yet supported by oxlint, the tool will warn you. For us, everything was covered, and we removed ESLint entirely in one pass.
oxfmt replaces Prettier and migrates the same way. It ships with a --migrate flag that reads your existing Prettier config and converts it to .oxfmtrc.json:
oxfmt --migrate prettier
oxfmt's output is designed to match Prettier's, so the diff when reformatting your codebase is minimal. Any differences are considered bugs by the oxfmt team.
Jack's setup used @storybook/test-runner with Playwright to run automated snapshot and accessibility tests. It also required http-server and wait-on to serve the built Storybook before tests could run.
We migrated to @storybook/addon-vitest, which runs Storybook tests inside Vitest. Because it spins up Storybook's dev server directly rather than needing a pre-built static version, we were able to remove http-server, wait-on, and the associated CI orchestration scripts entirely. The result is a simpler, faster test setup that integrates naturally with any existing Vitest configuration.
Setup is covered in the Storybook docs.
For the accessibility tests we added @storybook/addon-a11y, which integrates well with @storybook/addon-vitest, and lets us see the results directly inside Storybook, as well as failing in CI.
We also have a short vitest.setup.ts file that runs vitest's snapshot expect on every story being tested:
import { afterEach, expect } from "vitest"
import { page } from "vitest/browser"
afterEach(async () => {
const root = page.getByTestId("page")?.element()
if (!root) return
// oxlint-disable-next-line vitest/no-standalone-expect
expect(root.innerHTML).toMatchSnapshot()
})
With all of these changes combined, here's how our local build times compare:
| Command | Vite 5 / ESLint / Prettier (total) | Vite 8 / oxlint / oxfmt (total) | Speedup |
|---|---|---|---|
pnpm format | 3.634s | 1.548s | 2.3Γ faster |
pnpm lint:js | 3.131s | 1.122s | 2.8Γ faster |
vite build | 4.519s | 2.424s | 1.9Γ faster |
| Total | 11.284s | 5.094s | 2.2Γ faster |
Note: this comparison is Vite 5 β 8. In a separate codebase we saw Vite 7 β 8 alone drop build times from 6.02s to 1.74s, suggesting Vite 8 itself is a significant contributor beyond the oxc tooling changes.
CI times show a slight average improvement but vary too much to draw firm conclusions.
Unfortunately, I didn't to get a benchmark of the test run times, but I want to say it "feels" better.
Here's what the install command looks like now:
npm i -D vite postcss-custom-media oxlint oxfmt storybook @storybook/addon-a11y vitest @storybook/addon-vitestCompare that to the original:
npm i -D vite postcss-preset-env tinyglobby browserslist-to-esbuild prettier eslint @eslint/js eslint-config-prettier eslint-config-drupal storybook @storybook/test-runner playwright axe-playwright http-server wait-onFewer dependencies, faster tooling, less config to maintain. I'm sure we'll iterate more on this as newer things become available, so please shout out if you have any suggestions!
Tagged
Weβve just completed an extensive overhaul of our frontend tooling, with Vite and Storybook at the centre. Letβs go over it piece by piece.
We're proud to announce the release of vite-plugin-twig-drupal, a plugin for Vite that we hope will improve your workflow for front-end development with Drupal.

What do you get when you add Vite + Storybook + Pinto together to theme Drupal sites? Modern tooling thatβs easy to work with!