# detect-gpu: a benchmark score for the graphics card, so your app can scale its own quality

> detect-gpu classifies a GPU by its measured 3D rendering framerate and assigns a tier from 0 to 3, so a Three.js or Babylon.js application can pick sensible default settings without a user agent string. The package moved to the @pmndrs scope, is ESM only, and its benchmark data now loads from a CDN by default, which is the design decision that matters most for a production app.

**pmndrs/detect-gpu** — Classifies GPUs based on their 3D rendering benchmark score allowing the developer to provide sensible default settings for graphically intensive applications.

- Repository: https://github.com/pmndrs/detect-gpu
- Stars: 1,223 · Forks: 70
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/pmndrs-detect-gpu

## What it measures: a framerate, normalised, and a tier

detect-gpu uses rendering benchmark scores, meaning framerate normalised by resolution, in order to determine what tier should be assigned to the user's GPU, and the README compares it to a user agent detection for the GPU but more powerful. The tiering is simple and stated in full. If no WebGLContext can be created, or the GPU is blocklisted, or the GPU has reported rendering on less than 15 fps, then tier 0 is assigned, and the documentation says explicitly that one should provide a fallback to a non-WebGL experience. Otherwise the GPU is classified by the reported fps into tier 1 at 15 fps or above, tier 2 at 30 fps or above, or tier 3 at 60 fps or above. The higher the tier, the more graphically intensive workload you can offer the user. The result object carries the tier, an isMobile flag, the type, the fps and the GPU name string.

```ts
import { getGPUTier } from '@pmndrs/detect-gpu';

const gpuTier = await getGPUTier();

// Example output:
// {
//   "tier": 1,
//   "isMobile": false,
//   "type": "BENCHMARK",
//   "fps": 21,
//   "gpu": "intel iris graphics 6100"
// }
```

## The type field is the honest part, and it tells you what to trust

The result types table is more useful than the tier number, because it tells you how the answer was produced. BENCHMARK means a benchmark entry was matched and the fps reflects the measured framerate for that GPU. FALLBACK means the renderer was recognised but no benchmark match was found, and the tier is a conservative default. BENCHMARK_FETCH_FAILED means the benchmark fetch failed, whether from a CDN outage, a strict CSP or being offline, and the documentation says it is safe to retry. BLOCKLISTED means the renderer is on a known-bad list of drivers with severe issues, and the tier is always 0. WEBGL_UNSUPPORTED means no WebGL context could be created, again with tier 0. And SSR means the code is running server-side, with no window, so detection is skipped. The fps field is populated only for BENCHMARK results, and every other type leaves it undefined, which is a good design because it stops a consumer from reading a meaningful-looking number that is not one.

## The benchmark data comes from a CDN unless you host it yourself

By default, benchmark data is loaded from the UNPKG CDN, at a URL that includes the package version. The documentation explains how to serve it yourself, giving three reasons: offline environments, strict CSP, or to avoid a third-party CDN. The procedure is to download benchmarks.tar.gz from the repository and extract it into a publicly served directory, for example a public/benchmarks directory in your app, and then point getGPUTier at that URL with the benchmarksURL option. There is one detail that will break silently if you get it wrong, since the directory must be served at the exact URL passed in, because detect-gpu appends filenames like benchmarks-d-*.json to it when fetching. The repository ships the data as a benchmarks/ directory and a benchmarks.tar.gz at the top level, so the artefacts to serve are already in the tree.

```ts
const gpuTier = await getGPUTier({
  benchmarksURL: '/benchmarks',
});
```

## The data source stopped updating in December 2025

The README carries a note that deserves attention from anyone planning to depend on this. The benchmark data source, gfxbench.com, stopped updating in December 2025. The current data remains accurate for existing GPUs, but the project is exploring alternative data sources for future updates, and issue 132 tracks the progress. The practical consequence is specific and worth stating plainly. The tier boundaries are computed from a framerate measurement on the user's machine, but the lookup is a match against a static table of known GPUs, so a GPU released after the data froze will not be found in the table. Such a device falls into FALLBACK with a conservative default tier rather than being measured. The package also states that it uses both mobile and desktop benchmarking scores from gfxbench.com, and the live demo at pmndrs.github.io/detect-gpu/ lets you see the behaviour on your own hardware before you rely on it.

## The API surface: your own context, your own tier thresholds, and test overrides

The options object is documented in full in the README. benchmarksURL points at the directory where the benchmark data is hosted. glContext lets you pass in a WebGL context to avoid creating a temporary one internally. failIfMajorPerformanceCaveat determines whether to fail if the system performance is low or no hardware GPU is available, defaulting to false. mobileTiers and desktopTiers are the framerate per tier arrays, both defaulting to [0, 15, 30, 60], which is a genuinely useful knob because a mobile device at 30 fps is a different proposition from a desktop at 30 fps. And an override object lets you override the renderer string, whether the device is an iPad, whether it is mobile, the screen size, and how the benchmark data is loaded, which is used mainly for testing. The override block is what makes the library testable without a real GPU, and the tests are written with it.

## The package moved scope, and it is ESM only

Two packaging facts will affect an upgrade. First, the package has moved to the @pmndrs scope, and the README shows the migration as a diff, changing the import from detect-gpu to @pmndrs/detect-gpu. The old import path is what most tutorials and blog posts will show, so an upgrade touches more than a version number. Second, the Requirements section says the package is ESM only and CommonJS is not supported, which is a real constraint in a project that has not finished its migration to ES modules. The exports map in the package manifest confirms it, with only an import condition pointing at the ESM build and no require entry. The version is 6.0.26, the package is published to npm with public access, the package manager is pnpm 10.13.1, and the build is done with tsdown.

## The repository, the toolchain and the state of the project

The top level of the repository is a modern TypeScript package. There is a src/ directory, a test/ directory, a scripts/ directory, a benchmarks/ directory with the data and the tarball, an index.html and a local.html for the demo pages, a CHANGELOG.md, a LICENSE, a .nvmrc, an .oxfmtrc.json and an .oxlintrc.json for the formatter and linter, a tsconfig.json, a tsdown.config.ts for the build and a vitest.config.ts for the tests. The scripts are worth a glance, since the update-benchmarks script is node ./scripts/update_benchmarks.ts, which is what keeps the static table in step with the source, and the dev scripts run a watch build alongside a python3 http.server on port 8000 serving local.html. The test stack is Vitest with jsdom and coverage from v8, and the linting is oxlint with type-aware checking plus oxfmt for formatting, with esbuild as the only built dependency. The licence is MIT, the last push was on 2026-10-11, and the repository is not archived.

## Conclusion

detect-gpu suits a browser application that renders heavily and wants to adapt quality to the machine rather than assume a desktop GPU, and that is prepared to handle the tier 0 case with a non-WebGL fallback. It is the wrong choice if your application is a single canvas with fixed content, since the machinery then buys nothing, and a poor fit for a site with a strict Content Security Policy that cannot reach a third-party CDN, unless you self-host the benchmark data. Before adopting it, plan where the benchmark files will be served from, because the default is the UNPKG CDN, and note that the underlying benchmark source stopped updating in December 2025, so newly released GPUs will fall back to the conservative default rather than getting a measured tier.

## FAQ

### What does detect-gpu do?

It classifies GPUs based on their 3D rendering benchmark score, using framerate normalised by resolution, so a developer can provide sensible default settings for graphically intensive applications. The result is a tier from 0 to 3, where tier 0 is assigned if no WebGL context can be created, the GPU is blocklisted, or it reported under 15 fps.

### What do the different type values returned by getGPUTier mean?

BENCHMARK means a benchmark entry matched and fps is the measured framerate. FALLBACK means the renderer was recognised but no benchmark matched, so the tier is a conservative default. BENCHMARK_FETCH_FAILED means the fetch failed and is safe to retry. BLOCKLISTED and WEBGL_UNSUPPORTED always give tier 0. SSR means it ran server-side with no window, so detection was skipped. The fps field is only populated for BENCHMARK.

### How do I self-host the detect-gpu benchmark data?

Download benchmarks.tar.gz from the repository, extract it into a publicly served directory such as public/benchmarks, and pass benchmarksURL: '/benchmarks' to getGPUTier. The directory must be served at the exact URL passed in, because detect-gpu appends filenames like benchmarks-d-*.json to it when fetching.

### Is the detect-gpu benchmark data still being updated?

The README notes that the benchmark data source, gfxbench.com, stopped updating in December 2025. The current data remains accurate for existing GPUs, and the project is exploring alternative data sources for future updates, tracked in issue 132.

### Is detect-gpu still published as detect-gpu?

No, the package has moved to the @pmndrs scope, so imports change from detect-gpu to @pmndrs/detect-gpu. It is also ESM only, and CommonJS is not supported, so the package manifest exposes only an import condition.

## Sources

- [Issues](https://github.com/pmndrs/detect-gpu/issues)
- [License: MIT](https://github.com/pmndrs/detect-gpu/blob/master/LICENSE)
- [pmndrs/detect-gpu on GitHub](https://github.com/pmndrs/detect-gpu)
- [README](https://github.com/pmndrs/detect-gpu/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pmndrs-detect-gpu
