Quickstart

From install to a runnable Playwright suite and your first file:line findings, in one component and a handful of commands

Install

There are two channels and they ship different things, so pick by whether the target machine has Bun.

The standalone binary needs no Bun and nothing installed alongside it:

curl -fsSL https://raw.githubusercontent.com/lariocpt/flawless-react/main/install.sh | bash

The source package needs Bun >= 1.1, and every task works with no further setup because @biomejs/biome comes along as a real dependency:

bun add -g @lariocpt/flawless-react

One caveat before you pick the binary: it is self-contained Bun, but it cannot carry biome inside itself. perf-harness, unit-browser and perf-static work anywhere; a11y and react-rules shell out to biome and look for it in the project you are scanning (node_modules/.bin/biome), every ancestor workspace root, and then on PATH. It has to be biome 2 — those sweeps write a biome 2 linter config and read its JSON reporter, so a candidate of another major is walked past in favour of one that can read it, and if none can, the task fails naming the version and path it refused rather than reporting a sweep it never made. Any React codebase that already lints with biome 2 satisfies that for free — otherwise run bun add -d @biomejs/biome in that project, or use the source package. See Install for the full comparison.

With the source package you can also skip the install entirely and use bunx flawless-react, which is what the commands below do.

A component to point it at

Take a small dashboard:

// src/Dashboard.tsx
import type { FC } from "react";
import { useMemo } from "react";
import { Hello } from "./Hello.tsx";
import { Card } from "./Card.tsx";

interface DashboardProps {
  title: string;
  count: number;
}

export const Dashboard: FC<DashboardProps> = ({ title, count }) => {
  const items = useMemo(() => Array.from({ length: count }, (_, i) => i), [count]);
  return (
    <main>
      <h1>{title}</h1>
      <Hello name="alice" count={count} active variant="primary" />
      {items.map((i) => <Card key={i} title={`card ${i}`} body="hi" />)}
    </main>
  );
};

The positional argument can be a single file or a glob (src/**/*.tsx); every match becomes an entry component and runs against every selected task.

Run it

bunx flawless-react ./src/Dashboard.tsx --out .profile/dashboard

That analyses Dashboard.tsx, follows its locally-imported children, and scaffolds the performance harness into .profile/dashboard. A run with a path and no --tasks runs perf-harness only, and prints one line per task:

Summary:
  [ok] perf-harness   files:12 findings:0  scaffolded perf suite for 3 component(s): Dashboard, Hello, Card

Run the binary or the globally installed package the same way, without bunx:

flawless-react ./src/Dashboard.tsx --out .profile/dashboard

With no arguments at all, and on a TTY, you get an interactive menu instead: it asks for the path or glob, the output directory (default ./.flawless), and which of the seven tasks to run, with perf-harness and perf-static preselected. When the run finishes it offers to run the generated tests for you.

Findings

The scanning tasks report file:line findings. Ask for them by id:

bunx flawless-react ./src/Dashboard.tsx --out .profile/dashboard --tasks perf-static,a11y

Findings print grouped by file, as severity, line:column, rule and message, followed by the per-task summary:

src/Card.tsx
  ERROR  12:7  lint/a11y/useButtonType  Provide an explicit type prop for the button element.

Summary:
  [ok] perf-static    files:1 findings:0  static perf analysis: 0 finding(s) across 1 root(s)
  [ok] a11y           files:1 findings:1  a11y: 1 finding(s) across 1 component(s)

The same information is written to disk. Each scanning task writes a JSON report under <outDir>/reports/. perf-static reports per component (Dashboard.perf-static.json); a11y and react-rules are sweeps and write one aggregate each (a11y.json, react-rules.json). Every run writes <outDir>/reports/summary.json. A small <Component>.flawless.json is also dropped next to each analysed source file — counts per task, the three highest-severity findings, and the paths of the full reports — so issues surface where the code lives.

Run the generated suite

The output directory is a standalone project. Install its dependencies and run it:

cd .profile/dashboard
bun install
bun run test           # headless Chromium
bun run test:headed    # visible Chromium

The first time you run a generated suite you may need bunx playwright install chromium.

You should see Playwright report five tests per analysed component (mounts at defaults, variant matrix, click rerender, forced rerender × N, stress-click-rerender × 50) and console lines with timing summaries:

[Hello variants] {"min":0.4,"avg":1.1,"p50":0.9,"p95":1.8,"max":2.3,"n":2}
[Hello forced] {"min":0.3,"avg":0.7,"p50":0.6,"p95":1.1,"max":1.6,"n":10}
[Hello hooks] useState
[heap] stress-click-rerender: 11896752 -> 12595549 bytes (delta +698797) -> heaps/Hello-profile__Hello-stress-click-rerender-x-50__stress-click-rerender-after.heapsnapshot

After the run, ./heaps/ contains .heapsnapshot artefacts for every stress scenario. They load in Chrome DevTools’ Memory tab, and two of them compared there show what a scenario retained.

Budgets are warn-only on the first run — you measure before you know what “fast enough” means for a component. Once the suite is generated it belongs to you: the files that hold your work (budgets, provider fixtures, handlers, colocated specs) are kept on re-runs — only a never-edited handlers.ts refreshes, and only when the API spec moves. What gets generated walks through each emitted file, and Extending a suite covers turning the warnings into hard assertions.

If unit-browser was part of the run, the suite’s package.json also carries a test:unit script for the Vitest units, and test:browser for the functional browser specs when perf-harness ran too — they drive its host. test:perf comes from perf-harness itself, so a run with both ends up with all three however you ordered the tasks.

The three flags to know first

FlagEffect
--out <dir>Output directory for everything the run produces. Defaults to ./.flawless.
--tasks <csv>Comma-separated task ids, which skips the interactive menu. The ids are perf-harness, unit-browser, perf-static, a11y, react-rules.
--no-interactiveNever prompt. Requires a positional path or glob plus --tasks, which is what you want in CI or a script.

Task ids are validated, so a typo fails the run with the list of valid ids rather than doing something unexpected.

Everything else — --depth, --variants, --props, --provider, --api, --report, --run-tests, --json and the Next.js flags — is in the CLI reference.