この記事は現在英語版のみです。

(記事)

Web 3D Performance Engineering: Measurement, Budgets, and Acceptance

To make a 3D website run smoothly, first measure where the bottleneck is, set budgets, choose the fix that matches that bottleneck, and accept the result against numbers that can be reproduced.

Polish Design engineering team・2026-10-28

Contents

Performance problems in a 3D website fall into three segments, and the measurements and fixes for one do not carry over to another. Loading is how long the wait lasts before the product appears and can be operated. Memory is whether a phone can hold the scene without crashing. Per-frame cost is whether rotating and zooming stay smooth. Lumping all three into 'it is slow' makes it impossible to tell what to change.

What Core Web Vitals Can and Cannot Measure

LCP is the time to paint the largest image, text block, or video in the first screen, counted from the moment navigation starts; 2.5 seconds or less is good (Walton et al., 2019). INP summarizes the interaction latency of all clicks, taps, and key presses during a visit, takes the worst interaction while excluding outliers, and counts 200 milliseconds or less as good and 500 milliseconds or more as poor (Wagner et al., 2022).

CLS measures unexpected layout shift; 0.1 or less is good and above 0.25 is poor (Mihajlija et al., web.dev). By these definitions, none of the three covers when the product first appears inside the canvas, when it first becomes usable, or the per-frame time while a model rotates, so those need separate measurement points.

The WebGL recommendations in the MDN documentation (MDN Web Docs, 2026) are likewise organized in three parts: reduce draw calls, manage texture memory, and compile all shaders before linking. Draw calls and shader compilation fall under per-frame cost, and textures fall under memory and loading. The documentation gives principles only and no measured numbers.

A person leans in with both hands against the rim of a large semicircular gauge whose face runs from teal through yellow to red, with the black needle pointing to the right.

Separate Timing from Cost, Then Find the Bottleneck

Two very different problems in a 3D scene both show up as stutter. A timing problem is an animation that does not connect or a transition that pauses. A cost problem is every frame doing too much work. If the stutter is constant, measure frames per second, shader recompilations, and GPU time first; if it appears only during transitions, look at animation timing.

A common misdiagnosis of continuous stutter is adjusting animation timing again and again to no effect. The real causes can be two: the screen is locked into continuous rendering although the content is static, and interface annotations are rebuilt in batches at short intervals, so that when a third-party line component unmounts, it also releases materials still in use. Shaders then recompile several times per second. Diagnosis switched to intercepting WebGL calls: the number of clear calls approximates the frame count, and the number of linkProgram calls is the number of shader recompilations. drawElements runs in the hundreds per frame, which makes it a poor indicator.

The scene of a product configurator has about 60 draw calls and about 1 million triangles. Sweeping the pixel ratio across seven values and fitting a regression splits the per-frame cost into a fixed part and a part that grows with pixels. On desktop a frame costs about 4 ms, of which about 60% is fixed; in the mobile configuration a frame costs about 2.5 ms, of which about 80% is fixed. Fixed cost does not shrink when resolution drops.

Fig. 1Check cost first for constant stutter and timing only for stutter during transitions, then split cost into three paths
  1. Confirm the symptomConstant stutter, or only during transitions
  2. Measure costFrames per second, shader recompilations, GPU time
  3. Split the cost structureFixed cost versus cost that grows with pixels
  • Stutter only during transitions or switches
    Check animation timingWake-up duration, camera animation, frame catch-up order
  • Constant stutter, shaders recompile at a regular rhythm
    Find materials that are repeatedly created and releasedDo not rebuild interface annotations in batches
  • Constant stutter, fixed cost dominates
    Geometry: reduce and step down detail. Draw calls: merge drawsLower-detail models while dragging, texture atlases for static parts
  • Constant stutter, cost grows with pixels and layers
    Cut layers per pixel and material workLowering resolution alone may not help

Set Budgets Early and Convert Each Item

Budgets are settled at the start of a project and allocated item by item, like a spreadsheet. In a property-showroom website, a Chromium emulation of a phone used about 2.6 GB of memory. The target came in two stages: first about 0.9 GB, then tightening toward a lower peak for the whole flow, and the trees, night scene, and progressive sharpening added later all had to fit within the same budget. The item-by-item conversion:

  • Decoder workers shut down after each batch loads, and the CPU-side copies of textures and geometry are released once uploaded to the GPU.
  • On phones, the environment map, shadows, water reflection, and panorama cache are each stepped down.
  • Texture tiers are decided by the owner: the 1024 tier on phones, and a tier chosen by device on desktop.
  • On phones, geometry is split into chunks of no more than 65,536 vertices with 16-bit indices, and positions and normals are stored as quantized integers.

Features that are harmless alone but fail together can only be found by measurement. The night scene and the trees each added less than 5 MB at every step of the flow, yet turning both on at once jumped about 40 MB per step, because the cause is the process's memory high-water mark, not the sum of the two. The decision was to keep the trees on phones and leave the night-time city to desktop, which the owner accepted. A lightweight mobile night scene that lights only the towers was built later, and the peak increase over a long flow was about zero.

After the first stage, the memory high point of the whole flow was about 910 to 960 MB; after the second stage it fell to about 510 to 710 MB, with a single peak of about 750 MB and measurement noise of about ±40 MB. These numbers come from desktop Chromium emulating a phone. The budget table tracks every item instead of a single total.

First-screen download size is a second budget. The 3D configurator in the LIDROP case was inventoried in rounds with a fixed method: after each build, a headless browser reads every resource request and classifies the requests by file extension, and 3D files make up about 80%. Each round touches only bytes that are never used at runtime: round one took the first-screen download from about 57 MB to about 43 MB, and round two to about 37 MB. A comparison for optimized models appears in before-and-after numbers for 3D model optimization.

Fig. 2Two rounds of inventory cut the first-screen download from about 57 MB to about 37 MB, each round touching only data never used at runtime
  1. Before the inventoryabout 57 MBFirst-screen download
  2. Round oneabout 14 MBThree adjustments, all to data never used at runtime
  3. After round oneabout 43 MBFirst-screen download
  4. Round twoabout 6 MBThree adjustments on the asset side
  5. After round twoabout 37 MBFirst-screen download

LIDROP 3D configurator, same model throughout; after each build, a headless browser reads the resource requests and classifies the requests by file extension

Round three started from a different build. Embedded base-color images that are always replaced at runtime were shrunk to 4×4, the seat texture was lowered to 1024, and descriptive data was deferred, taking the size from about 36.5 MB to about 32.5 MB. The starting build differed from the first two rounds, so the three do not join into one curve. Whenever a part is added, the first check is whether its embedded texture gets replaced at runtime; if it does, shrink it.

Two further items would change the loading architecture: splitting files per model would save about 3.4 MB, and sharing one texture across four files would save about 2.5 MB.

A person grips a large wrench with both hands to turn the nut at the center of a yellow gear, with a smaller teal gear meshing beside it.

Choose the Fix by Bottleneck

The same symptom calls for different fixes depending on the bottleneck. Each row of the table is a measured result, not a general rule.

BottleneckWhat worksWhat fails or has a cost
Geometry and draw callsStand in a simplified-geometry proxy for the mirror pass and use lower-detail models while dragging; merge draws and use texture atlasesLowering resolution alone: fixed cost does not fall
Fill rate and material workReduce the layers per pixel; render lighting that can be precomputed offline into texturesPermanently lowering resolution: the image turns soft and gets no faster
Repeated shader recompilationAvoid rebuilding interface annotations in batches and do not release materials still in useAdjusting animation timing, which does not fix continuous stutter
One-time costBuild in slices under a per-frame budget of at most 2 ms, nearest to the camera firstBuilding more than a hundred objects at once, stalling a single frame for about 50 to 100 ms

The geometry-bound example used a GPU timer query to measure the GPU time of the main pass and the mirror pass separately: the mirror pass took about 1.1 to 1.2 ms, and a simplified-geometry proxy brought it below 0.2 ms (headless desktop with a phone-sized window; only interleaved ratios were trusted). In the offline-rendering example, the environment was rendered ahead of time into a panorama and a near-ground texture, and the two GPU passes on the phone tier fell from about 8.1 ms to about 4.7 ms. The cost is that reflections of the product in ground puddles are off by default, and trade-offs of this kind are listed for the client to decide before work starts.

The fill-bound example is a Gaussian-splatting (3DGS) scan scene: the average overlap measured about 180 layers against a healthy range of about 5 to 15, so reducing the number of Gaussians worked while lowering resolution alone made no difference in speed. The example of throttling updates while the camera moves comes from an event-map website: on an emulated phone, the reflection update dropped from 60 to 30 times per second, and per-frame JavaScript time during dragging fell from about 8.5 ms to about 6.5 ms.

Lower Resolution Works Only When Pixels Are the Bottleneck

Lowering resolution for smoothness is a common move; whether it works depends on the bottleneck, and every switch has a cost of its own. In one measured configurator, pixels dropped to about 36% of the original, yet frame time fell by only about 25% on desktop and about 15% in the mobile configuration, because fixed cost dominates. Each resolution change also reallocates the drawing buffer, so one drop and one restore costs about 20 ms on desktop and about 10 ms on mobile; break-even on desktop needs a continuous drag of about 0.4 seconds, and on mobile it never breaks even. That configurator therefore lowers resolution only while the loading screen hides the scene, and never during interaction. In another product the bottleneck was fragment shading of the material, so pixels were the cost, and on phones the drag therefore uses a fixed lower pixel ratio.

The Right Compression Format Flips with Network Speed

The Khronos EXT_meshopt_compression extension describes another geometry compression format built around fast decoding (Kapoulkine et al., Khronos). Textures in KTX2 with Basis Universal are transcoded by the engine into the block-compressed format the platform supports (Callow et al., 2020). Neither specification quantifies the savings, so how much is saved has to be measured.

Measured from the modelers' original files, converting to meshopt made files larger by 1.5 to 2.3 times on average, so the encoding has to be recomputed around first-visit experience. For one wheel model, draco downloads about 3.8 MB and decodes in about 1.2 seconds, while meshopt downloads about 6.4 MB and decodes in about 0.8 seconds. Counting download plus decode, draco is faster at or below about 46 Mbps and meshopt is faster above it, as at 100 Mbps. These numbers show that a crossover exists; the actual crossover point varies with device and network.

Fig. 3Pure draco has the smallest download but leaves nearly 97 MB of GPU geometry; quantizing first and then applying draco gets both
Download sizeGPU geometry memory
  • about 26 MB
    meshopt
    about 52 MB
  • about 16 MB
    Pure draco
    about 97 MB
  • about 21 MB
    Quantize, then dracoChosen
    about 52 MB

LIDROP, total of the 7 first-screen model files; a different measure from whole-page first-screen download

Pure draco gives the smallest files, but it decodes to full-precision floating-point data, and the geometry on the GPU reaches nearly 97 MB; quantizing first and then applying draco gets both, and became the default. Comparisons must start from the original files: converting a file that is already meshopt back to draco makes it 71% larger.

Loading Order: Show First, Add Fidelity Later

Loading is often slow not because the files are large but because the waiting is ordered wrongly. A common pattern keeps the loading screen until the whole product, about 80 to 100 MB, has loaded. The fix is to change the reveal condition so that two main bodies and six categories of key parts are each ready, with non-critical parts filled in after the reveal. The page then prefetches the key GLB files and the decoders from index.html, because the network sat idle while JavaScript was still parsing and the parts-list API had not yet responded. At 20 Mbps, the time for the decoders to arrive fell from about 12 to 13 seconds to about 6.5 to 7.4 seconds. Prefetching has two lines not to cross:

  • as=fetch must be paired with crossorigin=anonymous; without the pairing the file downloads twice, which is worse than no prefetch.
  • If the prefetch list exists in two copies (the HTML and a code constant), letting the copies drift apart only wastes downloads, and no error is ever raised.

When the reveal waits for the whole first-screen set, the time on a saturated line is nearly bytes divided by bandwidth. Under a 20 Mbps throttle, for example, a first screen of about 35 MB keeps the loading screen up for about 15.9 seconds; at about 2.5 MB/s the download takes about 14 seconds. After the request order is adjusted, the reveal comes at about 15.8 seconds, a difference within noise, so the only effective lever is downloading less before the reveal.

For a product with demanding visual fidelity, loading is split into two stages. Each color first loads a lightweight version holding geometry, a 2048 base color, and 1024 normal and material-parameter textures, about 10.7 MB, and the loading screen waits only for that; the high-resolution textures are fetched in parallel in the background, transcoded, and swapped in. An 8K texture uploads at about 89 MB at once and stalls a desktop for about 69 ms, so it is uploaded in slices of about 1 MB with at most 4 ms per frame.

Measurement used a local production build, cold cache, throttling, the median of three runs per cell, and headless with a real GPU. A phone on a 10 Mbps connection saw the product appear after about 12 seconds instead of about 24, the first-visit total on desktop grew by about 4 MB, and the screenshot after the upgrade is pixel-identical to the one from loading everything at once. A smaller texture saved as WebP made the product appear only about 0.35 seconds sooner, while temporary GPU use rose from about 10 MB to about 35 MB and the fine normal-map detail disappeared, so it was rejected.

Whatever cannot be seen before the user can act should never block the first interaction. On an event-map website over slow phone networks, the models of three islands, about 2.5 MB in all, load only after the page is interactive, and each island first enters as a shell holding its place. Before that point, images that appear only afterward are not downloaded, and the countdown page does not load the 3D JavaScript. On an emulated phone with slow 4G, time to interactive fell from about 60 seconds to about 48 seconds, then to about 41 seconds after the second batch of changes.

Fig. 4Only essential resources load before the page is interactive; whatever cannot be seen is added afterward

Before interactive

  • Main scene and key partsThe reveal condition waits only for these
  • Island shellsPositions come from constants; a placeholder holds the place until the model arrives
  • Images on the first screenLayout reserves space by width and height
Deferred models and imagesPlaceholder shells and reserved sizes

After interactive

  • Models of three islandsAbout 2.5 MB in all, swapped in and faded in on arrival
  • Images seen only laterNo URL is given before interactive
  • Pages that need no 3DThe countdown page loads no 3D JavaScript

Never block the first interactionWhatever cannot be seen is deferred and added one piece at a time once the user can act

An event-map website, adjustments measured on an emulated phone with slow 4G

Acceptance Means Reproducible Numbers

Acceptance rests on numbers that can be reproduced, not on a screen recording. Each cell states its conditions: the target device, the network throttle, the cache state, and how many runs the median is taken over. When the machine carries other load, a single run can drift by about 2 ms, so only ratios from interleaved runs are trusted. Acceptance has four columns:

  • Loading: when the product first appears and the first-screen download size, one cell for each device and network speed.
  • Memory: the high point and peak over the whole flow, compared on the same build with only a feature flag switched.
  • Per-frame cost: frame time while dragging and zooming, and the number of shader recompilations.
  • Fidelity: the screenshot after the upgrade compared pixel by pixel against the baseline.

Real-device verification is a cell of its own. Most numbers here come from emulation or headless measurement in a desktop environment, and any number not marked as a real device is not one. Where measurement error comes from, and which methods hold up, is covered in measuring Web 3D performance. The full scope of 3D website development is described in the 3D website development service.

The same method applied to a real buying path appears in the path of a 3D property showroom and previewing a custom sign before payment.

How tiered loading works on a 3D world map is covered in the front end of an online fair.

When This Approach Does Not Fit

  • The content has no interactive need: video is enough.
  • The target devices are old and the budget does not allow tiers: giving different devices different quality is what makes a budget table workable.
  • The decision maker wants only a striking picture and will not accept trade-offs for phones: every cell of the budget table is a trade-off, and someone has to decide.

Preparation Before an Evaluation

Four items are needed before an evaluation starts:

  • Target device list: how old the oldest supported model is determines how the budget is set.
  • Assets that must appear on the first screen: these set the baseline of the loading budget.
  • Existing model files: used to re-measure from the originals.
  • URL of the current version of the page: used to measure the first baseline.

Once these items are ready, inquire about a 3D website performance evaluation.

(お問い合わせ)

Start a project.

三つのステップで、判断に足る進め方と予算帯が得られます。

  1. 01最初の打ち合わせを予約:時間を選びます。完全な仕様は不要です。
  2. 02オンライン相談(15〜30分):進め方の方向性、予算帯、スケジュール案を提示。
  3. 03素材を確認したうえで、約3〜5営業日で見積書兼納品仕様明細書を提出します。
メール
hello@polish-design.com.tw
郵送先住所
(104) 台湾 台北市 中山区 植福路191-1号 3F

最初の打ち合わせを予約

平日 16:00〜18:00オンライン30分

先に時間帯を選び、Google アカウントまたはメールアドレスで確認します。予約が確定するとカレンダーに追加できます。

時間を選ぶ

または、先に内容を送信

まだ時間を決めていない場合は、先にプロジェクトの内容をお送りください。担当者がメールで返信します。送信後に続けて予約することもできます。

補足情報(任意):サービス項目、現在のフェーズ、既存リンク、知ったきっかけ

入力内容は、今回のプロジェクト評価にのみ使用します。

ISO/IEC 27001:2022 認証書

(Certificate)

ISO/IEC 27001:2022

認証機関
ARES International Certification
認証範囲
インタラクティブ没入型3Dシステムの開発プロセス、ならびにネットワークおよびインフラの運用・管理。

認証機関サイトで確認