webgl-gate 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/dist/index.d.ts +57 -0
- package/dist/index.js +87 -0
- package/dist/react.d.ts +9 -0
- package/dist/react.js +20 -0
- package/package.json +84 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleksandr Kutsenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,210 @@
|
|
|
1
|
-
#
|
|
1
|
+
# webgl-gate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Skip your WebGL bundle when the page is drawn by the CPU.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/Alexkutsenko95/webgl-gate/main/assets/webgl-gate-card.png" width="600" alt="Probe time in headless Chrome: 10 to 24 ms on a real GPU, so the scene loads; 1.4 to 1.7 s on SwiftShader, the CPU rasterizer PageSpeed uses, so the scene bundle is never downloaded.">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
Lighthouse and PageSpeed Insights measure mobile pages on **SwiftShader**, a software rasterizer. So do many
|
|
10
|
+
virtual machines and remote desktops, and so does any browser with hardware acceleration switched off. There a
|
|
11
|
+
PixiJS or three.js scene doesn't just run slowly. Downloading and parsing its bundle alone costs main-thread time,
|
|
12
|
+
and your Total Blocking Time goes with it. `webgl-gate` answers one question before you load that bundle: **is
|
|
13
|
+
there a real GPU behind this page?**
|
|
14
|
+
|
|
15
|
+
- **Under 1 kB:** 745 B gzip for the core, 223 B for the React hook.
|
|
16
|
+
- **No network, no benchmark tables, no dependencies.**
|
|
17
|
+
- **Checks the renderer name, not only `failIfMajorPerformanceCaveat`.** Headless Chrome on SwiftShader still hands
|
|
18
|
+
out a context with that flag set.
|
|
19
|
+
- **Off the critical path:** probes after `load`, when the browser is idle.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install webgl-gate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
Load the scene only on a real GPU. On the CPU the import is never called, so the bundle is never downloaded:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { loadIfFastWebGL } from 'webgl-gate';
|
|
33
|
+
|
|
34
|
+
const scene = await loadIfFastWebGL(() => import('./scene'));
|
|
35
|
+
if (scene) scene.mount(canvas);
|
|
36
|
+
// otherwise keep the static image that is already on the page
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With React (SSR-safe: `null` on the server and on the first client render, so hydration matches):
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useFastWebGL } from 'webgl-gate/react';
|
|
43
|
+
|
|
44
|
+
export function Hero() {
|
|
45
|
+
const fast = useFastWebGL();
|
|
46
|
+
return fast ? <LiveScene /> : <img src="/hero.avif" alt="" width={1200} height={630} />;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`LiveScene` should itself be lazy (`next/dynamic`, `React.lazy`), so its WebGL code stays in a separate chunk.
|
|
51
|
+
|
|
52
|
+
## Where it helps
|
|
53
|
+
|
|
54
|
+
Anything heavy that only looks good on a GPU and has a cheap static fallback:
|
|
55
|
+
|
|
56
|
+
- **Hero and background effects:** shaders, particles, animated gradients. Show the poster frame instead.
|
|
57
|
+
- **Game covers and playable demos** on a landing page (where this gate came from). Show a screenshot.
|
|
58
|
+
- **3D product viewers and configurators** (three.js, Babylon.js). Show product photos.
|
|
59
|
+
- **WebGL maps** (MapLibre GL, Mapbox GL). Show a static map image with a "open map" link.
|
|
60
|
+
- **GPU data visualisation** (deck.gl, regl). Show a pre-rendered chart.
|
|
61
|
+
|
|
62
|
+
## Recipes
|
|
63
|
+
|
|
64
|
+
### PixiJS or three.js, any framework
|
|
65
|
+
|
|
66
|
+
Keep the engine import inside the scene module, so the bundler puts it in its own chunk:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// scene.ts
|
|
70
|
+
import { Application } from 'pixi.js'; // or: import * as THREE from 'three'
|
|
71
|
+
export async function mount(host: HTMLElement) { /* create the app, append its canvas to host */ }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// main.ts
|
|
76
|
+
import { loadIfFastWebGL } from 'webgl-gate';
|
|
77
|
+
|
|
78
|
+
const scene = await loadIfFastWebGL(() => import('./scene'));
|
|
79
|
+
if (scene) await scene.mount(document.querySelector('#hero')!);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Next.js (App Router)
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
'use client';
|
|
86
|
+
import dynamic from 'next/dynamic';
|
|
87
|
+
import Image from 'next/image';
|
|
88
|
+
import { useFastWebGL } from 'webgl-gate/react';
|
|
89
|
+
|
|
90
|
+
const LiveCover = dynamic(() => import('./LiveCover'), { ssr: false });
|
|
91
|
+
|
|
92
|
+
export function Cover() {
|
|
93
|
+
const fast = useFastWebGL();
|
|
94
|
+
return fast ? <LiveCover /> : <Image src="/cover.avif" alt="" width={1200} height={630} priority />;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The static image is the Largest Contentful Paint on every device; the live cover replaces it only on a real GPU,
|
|
99
|
+
after the page has loaded.
|
|
100
|
+
|
|
101
|
+
### React Three Fiber
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { lazy, Suspense } from 'react';
|
|
105
|
+
import { useFastWebGL } from 'webgl-gate/react';
|
|
106
|
+
|
|
107
|
+
const Scene3D = lazy(() => import('./Scene3D')); // the file that imports @react-three/fiber
|
|
108
|
+
|
|
109
|
+
export function Viewer() {
|
|
110
|
+
const fast = useFastWebGL();
|
|
111
|
+
const poster = <img src="/product.avif" alt="Product" width={800} height={800} />;
|
|
112
|
+
return fast ? <Suspense fallback={poster}><Scene3D /></Suspense> : poster;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Vue 3
|
|
117
|
+
|
|
118
|
+
```vue
|
|
119
|
+
<script setup lang="ts">
|
|
120
|
+
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
|
121
|
+
import { probeWebGLWhenIdle } from 'webgl-gate';
|
|
122
|
+
|
|
123
|
+
const LiveScene = defineAsyncComponent(() => import('./LiveScene.vue'));
|
|
124
|
+
const fast = ref(false);
|
|
125
|
+
onMounted(async () => { fast.value = (await probeWebGLWhenIdle()).fast; });
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<LiveScene v-if="fast" />
|
|
130
|
+
<img v-else src="/hero.avif" alt="" width="1200" height="630" />
|
|
131
|
+
</template>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Svelte
|
|
135
|
+
|
|
136
|
+
```svelte
|
|
137
|
+
<script>
|
|
138
|
+
import { onMount } from 'svelte';
|
|
139
|
+
import { loadIfFastWebGL } from 'webgl-gate';
|
|
140
|
+
|
|
141
|
+
let Scene = null;
|
|
142
|
+
onMount(async () => { Scene = (await loadIfFastWebGL(() => import('./Scene.svelte')))?.default ?? null; });
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
{#if Scene}<svelte:component this={Scene} />{:else}<img src="/hero.avif" alt="" width="1200" height="630" />{/if}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### MapLibre GL
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { loadIfFastWebGL } from 'webgl-gate';
|
|
152
|
+
|
|
153
|
+
const maplibre = (await loadIfFastWebGL(() => import('maplibre-gl')))?.default;
|
|
154
|
+
if (maplibre) new maplibre.Map({ container: 'map', style: '/style.json', center: [30.52, 50.45], zoom: 11 });
|
|
155
|
+
// otherwise the <img> of the static map stays in #map
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### API
|
|
159
|
+
|
|
160
|
+
| Export | What it does |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `loadIfFastWebGL(load)` | Waits for an idle moment after `load`, then calls `load()` only on a real GPU. Returns the module or `null`. |
|
|
163
|
+
| `probeWebGLWhenIdle(timeout?)` | The verdict, `{ fast, reason, renderer }`, probed after `load` in `requestIdleCallback`. |
|
|
164
|
+
| `probeWebGL()` | The same verdict, synchronously. Memoized: one probe per page. On the server returns `fast: false` without caching. |
|
|
165
|
+
| `hasFastWebGL()` | `probeWebGL().fast`. |
|
|
166
|
+
| `isSoftwareRenderer(gl)`, `rendererName(gl)` | For a context you already have. |
|
|
167
|
+
| `SOFTWARE_RENDERER` | The regular expression: SwiftShader, llvmpipe, softpipe, "Software", "Basic Render". |
|
|
168
|
+
| `resetWebGLProbe()` | Forget the memoized verdict. |
|
|
169
|
+
|
|
170
|
+
`reason` is one of `gpu`, `software` (a context exists but the CPU draws it), `no-context` (WebGL is off or blocked,
|
|
171
|
+
or the browser reported a major performance caveat), `error`, `no-dom` (server).
|
|
172
|
+
|
|
173
|
+
## What it costs, measured
|
|
174
|
+
|
|
175
|
+
The same page in headless Chrome 155 on an Apple M2 Pro, three runs each:
|
|
176
|
+
|
|
177
|
+
| Renderer | Verdict | Probe time | Scene bundle requested |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| ANGLE Metal (Apple M2 Pro) | `gpu` | 10–24 ms | yes |
|
|
180
|
+
| ANGLE SwiftShader (`--use-angle=swiftshader`) | `software` | 1.41–1.73 s | **no** |
|
|
181
|
+
|
|
182
|
+
Read the second row carefully. On a CPU rasterizer, creating even one WebGL context is expensive, and that is
|
|
183
|
+
exactly where the answer is "no". That is why the default entry points wait for `load` and an idle callback
|
|
184
|
+
instead of probing during page load. If you call the synchronous `probeWebGL()` yourself, keep it off the critical
|
|
185
|
+
path too.
|
|
186
|
+
|
|
187
|
+
## Where it came from
|
|
188
|
+
|
|
189
|
+
This is the gate from [kutsikgames.com](https://kutsikgames.com), a game studio site with live PixiJS covers.
|
|
190
|
+
Together with inlined critical CSS and responsive AVIF covers, it took the site's mobile PageSpeed score from 78
|
|
191
|
+
(LCP 4.2 s) to 100 (LCP 1.7 s, TBT 0 ms). The gate is one of those changes, not all of them.
|
|
192
|
+
|
|
193
|
+
## When to use something else
|
|
194
|
+
|
|
195
|
+
`webgl-gate` answers "GPU or not". If you need to know *how strong* the GPU is, to pick quality tiers,
|
|
196
|
+
use [`detect-gpu`](https://github.com/pmndrs/detect-gpu): it classifies GPUs by benchmark scores and is much larger.
|
|
197
|
+
|
|
198
|
+
Browsers that hide the real renderer name (fingerprinting protection) may report a generic one,
|
|
199
|
+
so the gate sees a GPU. If that matters to you, time a real frame of your scene after mounting and fall back when
|
|
200
|
+
it is too slow.
|
|
201
|
+
|
|
202
|
+
## Not yet
|
|
203
|
+
|
|
204
|
+
The verdict is not cached across visits. A cached "software" answer can outlive the hardware it describes: the
|
|
205
|
+
user turns hardware acceleration back on, or a driver leaves a blocklist. Audit tools always start with a clean
|
|
206
|
+
profile anyway.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT © Oleksandr Kutsenko
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webgl-gate: decide, before you download a WebGL bundle, whether this page is drawn by a real GPU.
|
|
3
|
+
*
|
|
4
|
+
* Software rasterizers (SwiftShader, llvmpipe, softpipe, "Microsoft Basic Render Driver") run WebGL on the CPU.
|
|
5
|
+
* Lighthouse and PageSpeed Insights measure mobile pages on exactly that kind of renderer, and so do many
|
|
6
|
+
* virtual machines, remote desktops and browsers with hardware acceleration turned off. There a WebGL scene
|
|
7
|
+
* costs main-thread time and Total Blocking Time, and the user gets a slideshow instead of an effect.
|
|
8
|
+
*
|
|
9
|
+
* `failIfMajorPerformanceCaveat` is not enough on its own: headless Chrome with SwiftShader still hands out a
|
|
10
|
+
* context with that flag set. So the verdict also reads the renderer name.
|
|
11
|
+
*/
|
|
12
|
+
/** Renderer names of known CPU rasterizers. */
|
|
13
|
+
export declare const SOFTWARE_RENDERER: RegExp;
|
|
14
|
+
type TGL = WebGLRenderingContext | WebGL2RenderingContext;
|
|
15
|
+
/** The most specific renderer name the browser exposes: unmasked when allowed, otherwise `RENDERER`. */
|
|
16
|
+
export declare function rendererName(gl: TGL): string;
|
|
17
|
+
/** True when this context is rasterized on the CPU. */
|
|
18
|
+
export declare function isSoftwareRenderer(gl: TGL): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Why the gate decided as it did. `no-context`: WebGL is off or blocked, or the browser itself reported a major
|
|
21
|
+
* performance caveat. `software`: a context exists, but a CPU rasterizer draws it.
|
|
22
|
+
*/
|
|
23
|
+
export type TGateReason = 'no-dom' | 'no-context' | 'software' | 'error' | 'gpu';
|
|
24
|
+
export interface IGateResult {
|
|
25
|
+
/** A real GPU draws this page: loading the WebGL bundle is worth it. */
|
|
26
|
+
readonly fast: boolean;
|
|
27
|
+
readonly reason: TGateReason;
|
|
28
|
+
/** Renderer name, when a context was created. */
|
|
29
|
+
readonly renderer: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Probe once per page with a throwaway canvas and release the context right away. The result is memoized:
|
|
33
|
+
* call it from as many components as you like. On the server (no `document`) it answers `fast: false`
|
|
34
|
+
* without caching, so the client still gets its own probe.
|
|
35
|
+
*/
|
|
36
|
+
export declare function probeWebGL(): IGateResult;
|
|
37
|
+
/**
|
|
38
|
+
* The probe is cheap on a GPU (10–25 ms in headless Chrome on Apple M2 Pro) but expensive exactly where it
|
|
39
|
+
* answers "no": creating the first context on SwiftShader took 1.4–1.7 s of main thread in the same Chrome.
|
|
40
|
+
* So run it off the critical path: after `load`, when the browser is idle. This is what the React hook and
|
|
41
|
+
* `loadIfFastWebGL` do.
|
|
42
|
+
*/
|
|
43
|
+
export declare function probeWebGLWhenIdle(timeout?: number): Promise<IGateResult>;
|
|
44
|
+
/** Shorthand: `true` when a real GPU draws this page. Probes synchronously; prefer the idle form on load. */
|
|
45
|
+
export declare const hasFastWebGL: () => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Load a module only on a real GPU, after the page has loaded. Pass a dynamic import so the bundler splits it out:
|
|
48
|
+
*
|
|
49
|
+
* const scene = await loadIfFastWebGL(() => import('./scene'));
|
|
50
|
+
* if (scene) scene.mount(canvas); else keepStaticImage();
|
|
51
|
+
*
|
|
52
|
+
* On the CPU the import is never called, so the bundle is never downloaded or parsed.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadIfFastWebGL<T>(load: () => Promise<T>): Promise<T | null>;
|
|
55
|
+
/** Forget the memoized verdict (tests, or after the user toggles a quality setting). */
|
|
56
|
+
export declare function resetWebGLProbe(): void;
|
|
57
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webgl-gate: decide, before you download a WebGL bundle, whether this page is drawn by a real GPU.
|
|
3
|
+
*
|
|
4
|
+
* Software rasterizers (SwiftShader, llvmpipe, softpipe, "Microsoft Basic Render Driver") run WebGL on the CPU.
|
|
5
|
+
* Lighthouse and PageSpeed Insights measure mobile pages on exactly that kind of renderer, and so do many
|
|
6
|
+
* virtual machines, remote desktops and browsers with hardware acceleration turned off. There a WebGL scene
|
|
7
|
+
* costs main-thread time and Total Blocking Time, and the user gets a slideshow instead of an effect.
|
|
8
|
+
*
|
|
9
|
+
* `failIfMajorPerformanceCaveat` is not enough on its own: headless Chrome with SwiftShader still hands out a
|
|
10
|
+
* context with that flag set. So the verdict also reads the renderer name.
|
|
11
|
+
*/
|
|
12
|
+
/** Renderer names of known CPU rasterizers. */
|
|
13
|
+
export const SOFTWARE_RENDERER = /swiftshader|llvmpipe|softpipe|software|basic render/i;
|
|
14
|
+
/** The most specific renderer name the browser exposes: unmasked when allowed, otherwise `RENDERER`. */
|
|
15
|
+
export function rendererName(gl) {
|
|
16
|
+
const info = gl.getExtension('WEBGL_debug_renderer_info');
|
|
17
|
+
return String(gl.getParameter(info ? info.UNMASKED_RENDERER_WEBGL : gl.RENDERER) ?? '');
|
|
18
|
+
}
|
|
19
|
+
/** True when this context is rasterized on the CPU. */
|
|
20
|
+
export function isSoftwareRenderer(gl) {
|
|
21
|
+
return SOFTWARE_RENDERER.test(rendererName(gl));
|
|
22
|
+
}
|
|
23
|
+
let cached;
|
|
24
|
+
/**
|
|
25
|
+
* Probe once per page with a throwaway canvas and release the context right away. The result is memoized:
|
|
26
|
+
* call it from as many components as you like. On the server (no `document`) it answers `fast: false`
|
|
27
|
+
* without caching, so the client still gets its own probe.
|
|
28
|
+
*/
|
|
29
|
+
export function probeWebGL() {
|
|
30
|
+
if (cached)
|
|
31
|
+
return cached;
|
|
32
|
+
if (typeof document === 'undefined')
|
|
33
|
+
return { fast: false, reason: 'no-dom', renderer: '' };
|
|
34
|
+
try {
|
|
35
|
+
const canvas = document.createElement('canvas');
|
|
36
|
+
const gl = canvas.getContext('webgl', { failIfMajorPerformanceCaveat: true });
|
|
37
|
+
if (!gl)
|
|
38
|
+
return (cached = { fast: false, reason: 'no-context', renderer: '' });
|
|
39
|
+
const renderer = rendererName(gl);
|
|
40
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
41
|
+
const software = SOFTWARE_RENDERER.test(renderer);
|
|
42
|
+
return (cached = { fast: !software, reason: software ? 'software' : 'gpu', renderer });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return (cached = { fast: false, reason: 'error', renderer: '' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The probe is cheap on a GPU (10–25 ms in headless Chrome on Apple M2 Pro) but expensive exactly where it
|
|
50
|
+
* answers "no": creating the first context on SwiftShader took 1.4–1.7 s of main thread in the same Chrome.
|
|
51
|
+
* So run it off the critical path: after `load`, when the browser is idle. This is what the React hook and
|
|
52
|
+
* `loadIfFastWebGL` do.
|
|
53
|
+
*/
|
|
54
|
+
export function probeWebGLWhenIdle(timeout = 3000) {
|
|
55
|
+
if (cached || typeof window === 'undefined' || typeof document === 'undefined')
|
|
56
|
+
return Promise.resolve(probeWebGL());
|
|
57
|
+
return new Promise((done) => {
|
|
58
|
+
const run = () => done(probeWebGL());
|
|
59
|
+
const idle = () => {
|
|
60
|
+
if (typeof window.requestIdleCallback === 'function')
|
|
61
|
+
window.requestIdleCallback(run, { timeout });
|
|
62
|
+
else
|
|
63
|
+
setTimeout(run, 1);
|
|
64
|
+
};
|
|
65
|
+
if (document.readyState === 'complete')
|
|
66
|
+
idle();
|
|
67
|
+
else
|
|
68
|
+
window.addEventListener('load', idle, { once: true });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Shorthand: `true` when a real GPU draws this page. Probes synchronously; prefer the idle form on load. */
|
|
72
|
+
export const hasFastWebGL = () => probeWebGL().fast;
|
|
73
|
+
/**
|
|
74
|
+
* Load a module only on a real GPU, after the page has loaded. Pass a dynamic import so the bundler splits it out:
|
|
75
|
+
*
|
|
76
|
+
* const scene = await loadIfFastWebGL(() => import('./scene'));
|
|
77
|
+
* if (scene) scene.mount(canvas); else keepStaticImage();
|
|
78
|
+
*
|
|
79
|
+
* On the CPU the import is never called, so the bundle is never downloaded or parsed.
|
|
80
|
+
*/
|
|
81
|
+
export async function loadIfFastWebGL(load) {
|
|
82
|
+
return (await probeWebGLWhenIdle()).fast ? load() : null;
|
|
83
|
+
}
|
|
84
|
+
/** Forget the memoized verdict (tests, or after the user toggles a quality setting). */
|
|
85
|
+
export function resetWebGLProbe() {
|
|
86
|
+
cached = undefined;
|
|
87
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `null` during server rendering, the first client render (so hydration matches) and until the page has loaded
|
|
3
|
+
* and the browser is idle; then `true` on a real GPU and `false` on a CPU rasterizer or without WebGL. Render your
|
|
4
|
+
* static fallback while it is not `true`.
|
|
5
|
+
*
|
|
6
|
+
* const fast = useFastWebGL();
|
|
7
|
+
* return fast ? <LiveScene /> : <img src="/cover.avif" alt="" />;
|
|
8
|
+
*/
|
|
9
|
+
export declare function useFastWebGL(): boolean | null;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { probeWebGLWhenIdle } from './index.js';
|
|
3
|
+
/**
|
|
4
|
+
* `null` during server rendering, the first client render (so hydration matches) and until the page has loaded
|
|
5
|
+
* and the browser is idle; then `true` on a real GPU and `false` on a CPU rasterizer or without WebGL. Render your
|
|
6
|
+
* static fallback while it is not `true`.
|
|
7
|
+
*
|
|
8
|
+
* const fast = useFastWebGL();
|
|
9
|
+
* return fast ? <LiveScene /> : <img src="/cover.avif" alt="" />;
|
|
10
|
+
*/
|
|
11
|
+
export function useFastWebGL() {
|
|
12
|
+
const [fast, setFast] = useState(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let alive = true;
|
|
15
|
+
void probeWebGLWhenIdle().then((result) => { if (alive)
|
|
16
|
+
setFast(result.fast); });
|
|
17
|
+
return () => { alive = false; };
|
|
18
|
+
}, []);
|
|
19
|
+
return fast;
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webgl-gate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Skip your WebGL bundle when the page is rendered by the CPU (SwiftShader, llvmpipe, Basic Render). Under 1 kB, no network, no benchmark tables.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"webgl",
|
|
7
|
+
"webgl2",
|
|
8
|
+
"gpu",
|
|
9
|
+
"gpu-detection",
|
|
10
|
+
"software-rendering",
|
|
11
|
+
"swiftshader",
|
|
12
|
+
"llvmpipe",
|
|
13
|
+
"lighthouse",
|
|
14
|
+
"pagespeed",
|
|
15
|
+
"core-web-vitals",
|
|
16
|
+
"total-blocking-time",
|
|
17
|
+
"web-performance",
|
|
18
|
+
"lazy-loading",
|
|
19
|
+
"pixijs",
|
|
20
|
+
"threejs",
|
|
21
|
+
"react-three-fiber",
|
|
22
|
+
"babylonjs",
|
|
23
|
+
"maplibre",
|
|
24
|
+
"nextjs",
|
|
25
|
+
"react",
|
|
26
|
+
"vue",
|
|
27
|
+
"svelte"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Oleksandr Kutsenko (https://kutsikgames.com)",
|
|
31
|
+
"homepage": "https://github.com/Alexkutsenko95/webgl-gate#readme",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Alexkutsenko95/webgl-gate.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Alexkutsenko95/webgl-gate/issues"
|
|
38
|
+
},
|
|
39
|
+
"type": "module",
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"import": "./dist/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./react": {
|
|
47
|
+
"types": "./dist/react.d.ts",
|
|
48
|
+
"import": "./dist/react.js"
|
|
49
|
+
},
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
},
|
|
52
|
+
"main": "./dist/index.js",
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
],
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
66
|
+
"size": "node scripts/size.mjs",
|
|
67
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build && npm run size"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": ">=18"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"react": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@types/react": "^19.3.0",
|
|
79
|
+
"@types/react-dom": "^19.3.0",
|
|
80
|
+
"jsdom": "^26.1.0",
|
|
81
|
+
"react": "^19.3.1",
|
|
82
|
+
"react-dom": "^19.3.1",
|
|
83
|
+
"typescript": "^5.9.3",
|
|
84
|
+
"vitest": "^3.2.7"
|
|
85
|
+
}
|
|
86
|
+
}
|