On the same 3D page, memory measured during loading is only about 1/3 of memory after loading finishes. The canvas during loading is just 0.6 times its final size on each side, about 36% of the final area, and the remaining assets have not arrived yet. The number is not wrong. The moment of measurement is. If a vendor's report was taken at that moment, the figure looks far better than what users actually get. The overall approach to web 3D performance engineering covers what to measure first and what to change next; this article covers only how the measuring step goes wrong.
The development team accepts a performance number only under three conditions: rendering ran on a real GPU, measurement started after loading finished, and the app returns to baseline after a full round of interactions. If any one is missing, the number stays out of the comparison. The sections below cover where false numbers come from, how turning off vertical sync misleads, how to compare two builds, and what to check in a vendor's performance report.
Where False Numbers Come From
Screens captured during loading
During loading the scene is incomplete. Canvas size, textures, and geometry arrive in stages, so memory or frame rate measured at that point describes a half-built scene. A measurement script should assert that loading has finished before it starts sampling.

Comparing two builds needs the same discipline. The team builds each version separately and measures both with one script, never against a number remembered from earlier. On an interactive 3D product page, the older build measured about 800 MB and the newer build about 690 MB. Both numbers came from the same script, so the drop in memory is real.
Hidden tabs
In most browsers, requestAnimationFrame callbacks are paused in background tabs and hidden iframes to save performance and power, as MDN Web Docs, 2026 describes. If a render loop depends on that callback, a frame rate measured in a hidden tab cannot be taken as what users see. That last step is this article's inference; the MDN page does not discuss frame-rate measurement.
Software rendering
Without a GPU, Chromium can draw WebGL content on the CPU through SwiftShader, a software implementation of Vulkan and OpenGL ES. The Chromium project documentation describes this mechanism and notes that automatic fallback to SwiftShader is deprecated, so WebGL creation may simply fail. A headless browser on default settings runs software rendering, and the development team measured only about 1 to 3 fps that way. Software rendering also decodes KTX2 compressed textures into RGBA, which inflates memory numbers. The documentation does not say whether software-rendered performance represents real hardware, so the conclusion that it does not rests on those measurements.
Hidden tabs and software rendering together produce numbers absurd enough to spot at a glance: identical frame times for every setting, or about 2,200 frames per second, far above a typical display's refresh rate.
The wrong process
The memory figure shown for a Chrome tab covers the tab process only, not the GPU process, so both processes must be measured. On the same product page, lowering the desktop pixel-ratio cap reduced GPU process memory by about 70 MB, and that change does not appear in the tab process figure. The measurement used Playwright in headless mode with a real GPU, reading each process with top after fetching process IDs over CDP. Headless mode itself is not the problem; what matters is whether a real GPU is attached.
Normal numbers, empty scene
Some failures raise no error. Some Gaussian splat viewers with level of detail (LoD) enabled wait about 6 seconds and return 0 splats, with no error thrown. With LoD disabled, the same data returned about 3,000,000 splats. A script that only captures elapsed time would file the empty scene as an ordinary number. A measurement also needs to assert that content was actually drawn, for example that the drawn splat count is above 0.
Return to baseline
Absolute values are hard to judge in isolation. A more reliable test is whether the app returns to baseline after a full round of interactions. In a stacking test repeated for 4 rounds, the team counts geometries, textures, and shader programs after each round; flat counts mean nothing is accumulating with the interactions.
- Confirm rendering ran on a real GPUFrame rate and memory from software rendering are rejected
- Confirm loading finished, the tab was in the foreground, and content was drawnNumbers from loading and hidden tabs are rejected
- Finish a round of interactions and compare with baselineMeasure both the tab and GPU processes
- All three holdAccept as a single-run numberStill compare against the other build, interleaved
- Any one condition failsReject and measure againNumbers are not corrected afterward
- The app does not return to baseline after a roundLook for what accumulatesAbsolute values are not compared
Distortion From Turning Off Vertical Sync
Vertical sync aligns drawing with the display refresh, commonly 60 Hz. With vertical sync and the frame-rate cap turned off, a GPU-heavy scene leaves the CPU waiting on the GPU. The measurement shows each render taking a long time, which is easy to read as slow code, while the actual cause is the CPU waiting.
Take a 3D map built to run smoothly on phones. Measured with vertical sync off, each render after entry took about 20 ms, which looked like the cost of drawing every object for the first time, which makes a sliced warm-up tempting as a way to spread that cost. Measured again with vertical sync on, the first 40 renders after entry took only about 1 to 6 ms, and the existing precompilation plus warm-up was already enough. Sliced warm-up showed no measurable benefit and is marked not adopted.
- about 20 msTime per render after entryUse the sync-on figureabout 1 to 6 ms
First 40 renders after entry in a 3D map, approximate values; vertical sync on means a true 60 Hz
Jank needs to be classified before it is fixed. The team uses Chrome traces together with V8 sampling to sort long frames into JavaScript, style recalculation, layout, paint, garbage collection, and waiting on the GPU. A long frame of about 300 ms on desktop turned out to come mainly from the browser starting the audio device; with that step removed, the long frame dropped to about 30 ms. Without the classification, that long frame is easy to blame on code.
Comparing Two Builds
Noise in a single run can exceed the improvement. When other jobs were running on the machine and the system load average climbed from tens to over a hundred, the noise floor for the share of janky frames in each area was already about 2%. When an improvement is only a few percentage points, noise of that size can hide it, so single-run frame counts cannot be compared. The longest frame and the JavaScript milliseconds in a trace were affected less, but still need confirmation across multiple rounds.
- Turn vertical sync on and measure under a true 60 Hz condition.
- Simulate a phone by throttling the CPU 4 times, and label the result as simulated along with the throttling rate.
- Measure the noise floor first: repeat one build and see how widely its own numbers scatter. If the improvement is smaller than that spread, draw no conclusion.
- Interleave the two builds, A and B, so noise is shared evenly between the two.
- Run multiple rounds and take the median, not the single best run.
Each build is built separately and measured with the same script; the number of rounds follows the noise floor and variation
This problem is not unique to 3D. Mytkowicz et al. argue that seemingly insignificant details of an experimental setup can lead systems researchers to wrong conclusions, which is measurement bias; the authors observed the effect across several architectures and compilers (Mytkowicz et al., 2009).
Performance comparison calls for statistical methods rather than single runs. Georges et al. make this case for Java performance evaluation (Georges et al., 2007); in an example from the paper, taking the best of 30 runs makes two garbage collectors look similar, while a 95% confidence interval analysis shows one of the two is clearly better. This article quotes only that claim and example, not conclusions from the full text. The number of repetitions should follow variation: Kalibera and Jones, 2013 use a cost model of experiments to derive the repetitions needed at each level for a given precision, and note that repeating is most useful where uncertainty is greatest.
Optimizations Must Be Rejectable
Measurement has another use: rejecting optimizations. An adjustment that shows no measurable benefit is marked not adopted, with its measurement conditions and the reason written down so the next person does not repeat it. The sliced warm-up above is one such case.

Lowering resolution is a common adjustment, but it works only when pixels are the bottleneck. On an interactive 3D product page, cutting pixels to 36% reduced desktop frame time by only about a quarter, and every resolution switch reallocates the drawing buffer: one drop and restore cost about 20 ms, and break-even came only after dragging continuously for about 370 ms. So resolution is lowered only during loading, not during interaction.
Reducing how often the water reflection updates while the camera moves was measured on a simulated phone (CPU throttled 4 times, vertical sync on, two builds interleaved), and it cut about 2 ms from per-frame JavaScript time during dragging. Gains like this are counted in milliseconds, so conditions have to be stated. In the same batch of work, adding will-change: transform to landmarks and icon buttons made no difference in A/B, and the notes say not to try it again.
Rejection also happens at a larger scale. For a Gaussian splatting walkthrough that writes depth first with a mesh to cull splats behind walls, the occluder model lines up with the scan at walls and floors, but indoor objects do not line up, and at eye level, where nearby objects fill much of the screen, the error grows into large holes. This approach does not work for indoor scan assets of this kind, so it is off by default and the reason is written down to avoid repeated effort.
The MDN guidance on WebGL (MDN Web Docs, 2026) has three parts: reduce draw calls, manage texture memory, and compile shaders before linking. The page gives principles, with no measured numbers. It can point to where to look first, but whether a given change helps in a specific scene still has to be measured.
When memory changes, the cause matters too. The team takes a heap snapshot, finds ArrayBuffer nodes, and follows the retainers upward to the first known class. In one change, ArrayBuffer memory fell from about 230 MB to about 160 MB after GLB parsing finished and the parser was released, so the attribution has evidence behind it. For the model-side before-and-after comparison, see 3D model optimization before and after.
Checking a Vendor's Performance Report
When reviewing a vendor's performance report, read the conditions before the numbers. The table below lists each condition, what should be visible, and the risk when it is missing. It can be used alongside how to evaluate and choose a software vendor.
| Condition | What should be visible | Risk when missing |
|---|---|---|
| Rendering environment | A real GPU and device model stated; for headless, how a GPU was obtained | May be software rendering, so frame rate and memory carry no weight |
| Measurement timing | Sampling begins after loading finishes, with a stated way to confirm it | Numbers taken during loading run low; memory is only about 1/3 of the finished state |
| Device | A real device model; if simulated, labeled as simulated with the throttling rate | Simulated and real-device numbers are not interchangeable |
| Vertical sync | Stated as on or off, with the refresh rate when on | With it off, GPU waiting is counted as code cost |
| Rounds and statistics | The number of interleaved rounds, and the median or other statistic used | A single number cannot rule out noise |
| Noise floor | The spread of repeated runs of one build | No way to tell whether the gain exceeds noise |
| Processes measured | Whether both the tab process and the GPU process were measured | Changes in the GPU process go unseen |
| Return to baseline | Whether geometry, texture, and program counts return to the start after a round of interactions | Resources piling up round after round are mistaken for normal usage |
If any row cannot be filled in, the number may still be true, but there is no way to tell. In 3D development, the development team measures performance under this same set of conditions.
When This Does Not Fit
- The page has no interactive 3D: ordinary Web Vitals tools are enough, and the interleaved, multi-round approach in this article is not needed.
- A single number is all a slide needs and it does not have to be reproduced: one measurement is enough, with no interleaving or extra rounds, but the real GPU and finished loading conditions still apply.
Next Step
Prepare the existing performance report with its measurement conditions, and the devices that matter most. To see what the development team has built first, browse the case studies; when ready, contact the development team.




