Skip to content

Modernized version of js-framework-benchmark with Vite as a central bundler, and Tailwind CSS instead of BS3

License

Notifications You must be signed in to change notification settings

itsjavi/frontend-framework-benchmarks

Repository files navigation

Frontend Framework Benchmarks

Performance benchmarks for major frontend frameworks, based on js-framework-benchmark and @remix-run/component benchmarking code. The project is modernized to use Vite as the central bundler and Tailwind CSS for styling instead of Bootstrap 3.

Compared to js-framework-benchmark, this repo adds:

  • Sorting rows ascending (#sortasc)
  • Sorting rows descending (#sortdesc)
  • Pokeboxes: a completely new set of benchmarks that are much more DOM-heavy than the default ones

Table of Contents

Getting Started

Prerequisites:

  • Node.js
  • pnpm

Install dependencies:

pnpm install

Run the benchmarks:

pnpm run bench

This builds all frameworks, then uses Playwright to run the benchmarks and print results in the terminal.

Commands

  • pnpm run dev - Start the Vite dev server with HMR on port 4555.
  • pnpm run build - Build all frameworks and the main benchmark pages to dist/. directories.
  • pnpm run bench - Build all frameworks and run the Playwright benchmark suite.

Tech Stack

  • Bundler: Vite (all frameworks use Vite with framework-specific plugins for consistent builds)
  • Styling: Tailwind CSS 4
  • Test Runner: Playwright

Project Architecture

The benchmark runner uses a single Vite server and a pair of custom plugins to host everything in one build and dev pipeline:

  • Multi-HTML routing (plugins/vite-plugin-multi-html.ts)

    • Maps multiple page roots into one Vite app using pageDirs in vite.config.ts
    • Current routing:
      • / -> src/pages/**/index.html
      • /frameworks -> frameworks/*/index.html
    • Dev server rewrites incoming URLs to the correct index.html file
    • Build mode flattens HTML output so src/pages/**/index.html lands in dist/ without the src/pages prefix
    • Rollup entry points are generated from the directory scan so each page gets its own JS entry
  • Multi-framework transforms (plugins/vite-plugin-multi-framework.ts)

    • Scopes framework Vite plugins by file suffix (e.g. .react.tsx, .solid.tsx)
    • Ensures per-framework JSX transforms do not bleed into other implementations
    • Includes a guard to reset any global JSX config that plugins might apply
  • TypeScript project layout

    • Root tsconfig.json is a solution config with project references only
    • Shared compiler options live in tsconfigs/tsconfig.base.json
    • Each framework has its own tsconfig extending the base with JSX settings and scoped includes
    • This preserves per-framework typing while keeping the build orchestration centralized

Results Viewer

The results page loads past runs from results/bench-results-index.json and lets you select a run (with an optional comparison run) from a dropdown.

  • Each run writes a timestamped snapshot file in results/.
  • The latest run is still written to results/bench-results.json.
  • History retention is controlled by benchmarks.resultsRetention in package.json (default: 30).
  • You can label a run with --machine "Your machine label" and it will display above the config.

The results viewer fetches snapshots from /data/ in dev and /benchmarks/data/ in production. In dev, plugins/vite-plugin-bench-results.ts serves files from results/. For static hosting, copy results/ into the build output under data/ (or benchmarks/data/ when using build:prod).

Manual UI Tests

View the benchmark implementations

You can view the benchmark implementations at http://localhost:4555/, for example http://localhost:4555/frameworks/vani/.

Add a manual UI test

Manual tests live under src/pages/manual-tests/ and are served by the same Vite app (they are not part of the benchmark runner).

Each test should have its own directory:

src/pages/manual-tests/
  [test-name]/
    index.html
    index.ts[x]?

Also add it to the homepage list in src/pages/index.ts so it appears under "Other Tests".

Adding a Framework

Frameworks are configured in one shared Vite app, so adding a new one means wiring together config, TypeScript projects, and the framework directory.

  1. Create the framework directory at frameworks/<id>/ with:

    • index.html (use ../../src/styles.css and the .bench-shell body class)
    • index.ts that bootstraps the framework implementation
    • app.<suffix> (see file suffix rules below)
  2. Register the framework in package.json:

    • Add the framework runtime to dependencies (plus @types/* if needed).
    • Add the Vite plugin to devDependencies when the framework needs one.
    • Add a new entry to benchmarks.frameworks with:
      • id (slug used in URLs and runner output)
      • name (display name)
      • path (must be frameworks/<id>)
      • package (dependency name used for version lookup)
      • version (optional override)
      • implementationNotes (optional results notes)
  3. Wire up Vite plugins and file suffixes:

  • Add the framework to plugins/vite-plugin-multi-framework.ts and map its plugin.
  • Update the frameworkIds list and choose the file suffix (.react.tsx, .solid.tsx, etc).
  • Add the framework id to frameworks in vite.config.ts so Vite loads the plugin.
  1. Add a TypeScript project:
  • Create tsconfigs/tsconfig.<id>.json extending tsconfig.base.json.
  • Include ../src/**/*.ts and ../frameworks/<id>/**/* in include.
  • Add the new project to the references in tsconfig.json.

Framework File Suffixes

The multi-framework plugin scopes transforms by file suffix, so JSX-based frameworks need a framework-specific extension for their app entry:

  • React: app.react.tsx
  • Preact: app.preact.tsx
  • Remix: app.remix.tsx
  • Solid: app.solid.tsx
  • Svelte: app.svelte
  • Vue: app.vue
  • Vani/Vanilla: app.ts

Implementation Guidelines

The following rules are adapted from js-framework-benchmark and apply to framework benchmark implementations in this repo:

  • Reuse the shared helpers in src/core so implementations share baseline behavior and avoid accidental slow paths that skew results.
  • Keep the initial HTML rendered by the framework identical to the reference (in src/core/blueprint-*.html), including aria-hidden attributes. The runner will run a preflight check to ensure the initial HTML rendered by the framework is identical to the reference.
  • Do not change button IDs (run, runlots, add, update, clear, swaprows, sortasc, sortdesc). Playwright will use these IDs to interact with the UI.
  • Use the src/design-system.css classes to style the UI. You may use up to 3 classes per element, including Tailwind classes and utilities.
  • Do not use more than 3 CSS classes per element. If you need more than 3, consider rethinking it or defining a new component class in the src/design-system.css file.
  • Use the shared styles.css from the src directory (do not use other Bootstrap copies).
  • Avoid Shadow DOM; it breaks global CSS and automated tests.
  • Use keyed list rendering when the framework supports it: each row uses a stable id as its key so DOM nodes stay aligned to data items during updates, reorders, and swaps. For example, in React use the key prop to achieve this.
  • Prefer idiomatic framework code; avoid artificial micro-optimizations that skew results.
  • Do not use requestAnimationFrame to alter benchmark operations.
  • Do not add custom repaint timing code; the benchmark runner already measures timings.
  • Track selection as a single id or reference, not per-row flags.
  • Manual DOM manipulation or explicit event delegation may be flagged in reviews, with the exception of the vanilla JS implementation.

Publishing Bench Snapshots

Benchmarks are currently run on a MacBook Pro Apple M1 Max 32GB, macOS 26 machine to keep the environment controlled and results consistent, so the results are not representative of all machines. To publish a new snapshot:

  1. Use a machine with the same specs as the one used to run the benchmarks.
  2. Run the benchmarks with all other apps closed.
  3. Avoid heavy background processes (Docker, Ollama, etc.).
  4. On macOS, run killall bun || true && killall node || true && sudo purge to stop any running Node/Bun processes, clean the file cache, and free up memory. Keep at least 22GB free of the 32GB of RAM.
  5. Run pnpm run bench and wait for it to finish. Do not move or keep your mouse over the browser window while it runs.
  6. Verify the results with pnpm run dev, then push them to the repository.

FAQ

Why not automate the benchmarks and run them on GitHub Actions?

GitHub Actions are not reliable enough to run the benchmarks because the performance of the machines varies too much between runs, leading to drastically different results every time.

About

Modernized version of js-framework-benchmark with Vite as a central bundler, and Tailwind CSS instead of BS3

Topics

Resources

License

Stars

Watchers

Forks