Skip to main content
ASoc
Comparison

Playwright vs Vitest: Why This Repo Runs Both for Unrelated Jobs

Vitest is a committed dependency gating 305 tests on every push. Playwright is deliberately not a dependency at all — installed ad hoc for a script that runs a few times a year.

The ASoc Team8 min read

Playwright and Vitest aren't really competitors: Vitest runs assertions against your code, Playwright drives a real browser. Most comparisons frame the choice as "which one for testing," but this storefront runs both for entirely different jobs — Vitest gates every push as the actual test suite — 11 lines of config and 331 tests — and Playwright never runs a single assertion here at all. It's a screenshot pipeline, installed on demand, absent from package.json on purpose.

What each tool actually does in this repository

package.json tells the first half of the story on its own:

"devDependencies": {
  "vitest": "^4.1.9"
}

No playwright entry, anywhere. npm test runs vitest run against the invariant suite — catalog data, blog registry cross-checks, filter logic, the SEO keyword pipeline — and as of this post, that's 305 tests across 31 files, finishing in under three seconds:

 Test Files  31 passed (31)
      Tests  305 passed (305)
   Duration  2.94s

Playwright shows up nowhere in that run. It's used by two scripts under scripts/release/capture-screenshots.mjs and capture-gallery.mjs — that boot a real Chromium, navigate a template's live preview, and save product-page screenshots. Neither script makes an assertion. There's no expect(), no pass/fail, no CI gate. It's browser automation for content generation, not testing.

The dependency that's deliberately not a dependency

The most interesting fact about Playwright in this codebase is in a comment, not in code that runs:

// scripts/release/capture-gallery.mjs
// playwright is NOT a dependency of the storefront (it would cost every CI
// install for a script that runs a few times a year) -- install it ad hoc, the
// same way capture-screenshots.mjs expects:
//   npm install --no-save playwright
import { chromium } from "playwright";

That's a deliberate cost decision, not an oversight. Playwright's Chromium download alone is on the order of 150+ MB, and npm ci runs on every single push to this repo. Paying that weight on every CI run, every contributor clone, and every deploy — for a script invoked a handful of times a year when a new template needs screenshots — is a bad trade. npm install --no-save gets the browser onto the machine that's about to run the script without ever touching the lockfile that every other install has to resolve.

Vitest gets the opposite treatment for the opposite reason: it runs on every push, so its ~3-second cost is paid constantly and needs to stay a committed, versioned dependency everyone gets automatically.

Vitest herePlaywright here
In package.json?Yes, devDependenciesNo — installed ad hoc
Runs onEvery push (CI)A few times a year, manually
What it doesRuns 305 assertionsDrives a browser, takes screenshots
Failure modeBlocks the PRN/A — no assertions to fail
EnvironmentNode, jsdom-free (see below)Real Chromium
Cost paid byEvery npm ciOnly the machine running the script

Why Playwright's role here needed its own workaround

Screenshot capture isn't the interesting engineering problem — this sandbox's egress proxy is. Chromium can't use it directly; every request made from inside the browser process dies with ERR_CONNECTION_RESET. The fix, already documented in the Playwright screenshot-automation post, is to intercept all of Chromium's traffic and re-issue it through Node's own fetch, which does honor the proxy via NODE_USE_ENV_PROXY=1. That post owns the full mechanics; this one only needs the headline fact — Playwright here is infrastructure for a content pipeline, with its own environment quirks, not a testing framework competing with Vitest for the same job.

Where Vitest's speed comes from

The other half of the "why not the same tool" answer is architectural. Vitest's config for this project is small on purpose:

// vitest.config.ts
export default defineConfig({
  resolve: { alias: { "@": path.resolve(__dirname, "src") } },
  test: { include: ["src/**/__tests__/**/*.test.ts"] },
});

No browser, no DOM shim, no jsdom — every test here is pure logic: does the catalog have a valid latestVersion for every product, does every blog post's relatedTemplates slug actually exist, does the filter function match the right products for a given query. None of that needs a rendered page, so Vitest never pays for one. That's the actual reason it finishes 305 tests in under three seconds and Playwright needs a whole browser boot per screenshot — they're not two solutions graded on the same benchmark, they're solving different problems that happen to share the word "test" in casual conversation.

When you'd actually reach for each

  • Vitest (or an equivalent unit runner) for anything that's a pure function of data: validation logic, data transforms, invariant checks — the shape of everything in this repo's own suite.
  • Playwright for anything that needs a real rendered page and a real browser engine: visual regression, cross-browser rendering checks, or — as here — generating screenshots that have to reflect what a visitor's browser actually paints, not a simulated DOM.
  • Both, in the same project, for different jobs — which is the actual state of this codebase, and the answer most "X vs Y" framings skip past because it doesn't produce a winner.

If your project also runs E2E assertions through Playwright (expect(page).toHaveText(...)), the comparison changes — now it genuinely competes with Vitest's browser mode for that specific job, and the deciding factors are cross-browser coverage (Playwright: Chromium, Firefox, WebKit) versus how much of your existing Vitest config and mocks you'd rather keep reusing (Vitest browser mode runs your existing unit tests' tooling against a real DOM). This repo never reaches that fork, because it has zero E2E assertions of any kind — Playwright's only job here is pixels, not pass/fail.

What this means for CI cost specifically

The npm ci step on every push to this repo installs exactly what's in the lockfile — Vitest included, at its pinned ^4.1.9, resolved once and cached like every other devDependencies entry. Nothing about running 305 tests requires spinning up a browser process, so the CI runner never pays for one. Had Playwright been added as a devDependency instead of installed ad hoc, every one of those runs — plus every contributor's local npm ci, plus every fresh clone — would additionally download and cache a Chromium binary that gets used, in practice, only when someone runs one of two release scripts by hand. That's the concrete shape of the tradeoff the comment in capture-gallery.mjs is naming: dependency weight should track how often a tool actually runs, not just whether the codebase uses it at all.

Mistakes and how they show up

MistakeHow it shows upFix
Adding Playwright to devDependencies for occasional scriptsEvery npm ci pays a large download for a tool used a few times a yearInstall ad hoc (npm install --no-save) for infrequent, non-CI use
Reaching for Playwright to test pure logicSlow tests that boot a browser to check a data transformUse Vitest (or any unit runner) — no browser needed for non-visual logic
Assuming "uses Playwright" means "has E2E tests"A screenshot/automation script gets miscategorized as test coverageCheck whether the script makes assertions; browser automation and testing aren't the same thing
Running Chromium through a proxy without intercepting its trafficEvery browser-driven request dies with a connection reset in a proxied sandboxRe-issue requests through Node's own fetch, which honors the proxy env var
Picking one tool because the SERP frames it as "vs"Missing that a project can genuinely need both for unrelated jobsAsk what job needs doing — logic assertions or real-browser rendering — before picking a tool

Frequently asked questions

Can Vitest do what Playwright does? Vitest has a browser mode that runs tests against a real browser via WebDriver/Playwright's own engine — but that's still assertion-driven testing, not general browser automation. Vitest doesn't drive a browser to screenshot a live page the way this repo's release scripts do.

Can Playwright replace Vitest for unit tests? Technically you could write assertions in a Playwright script, but you'd be booting a full browser to check something like a data-validation function that has no UI at all — paying browser-boot latency for a job that needs none of it.

Why isn't Playwright in package.json if the repo genuinely uses it? Because "uses it" here means "a script imports it a few times a year," not "every install needs it." The ad hoc npm install --no-save pattern gets the tool onto the one machine running the script without adding weight to the other 99% of installs that never touch it.

Does this mean Playwright and Vitest are never real competitors? They compete specifically where a project runs browser-driven E2E assertions and is choosing a runner for that job — Playwright's own test runner versus Vitest's browser mode. This repo doesn't have that job at all, which is exactly why the "vs" framing doesn't fit it.

Templates in this post

ASoc Flow markets a workflow-automation product with a full 3-tier pricing table and an Automation Hub dashboard preview. ASoc Folio is a single-page developer portfolio with a filterable work grid and a built-in blog. ASoc Forge covers an AI resume-builder landing page with 15+ template previews and an AI-prompt panel mockup.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Comparison9 min read

Qwik vs. Next.js: Resumability vs. Not Shipping the Component

Qwik ships near-zero JS through resumability; this site's RSC split already does that for 9 of 10 home-page sections. Where the real cost still lands: one accordion.

Read more
Comparison8 min read

React vs. Gatsby: The Real Question Is the GraphQL Layer

Gatsby adds a GraphQL data layer on top of React. This catalog imports typed data directly instead, with zero GraphQL and zero content plugins.

Read more