Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 16.5Advanced ~21 min · shot 5 of 6

Loading, Size and Failure States

In one line

A designer-made animation arrives late and can fail. Load the player only when the canvas is near the screen, show a still poster, handle loadError, stop drawing off screen, and give reduced-motion visitors one still frame.

0/6 · 0%

Think of it like this

A shop window with a clockwork display. The shopkeeper winds it only when someone stops to look, shows a painted card while it is being set up, switches it off when no one is near, and keeps the card up if the mechanism breaks. A good animation on a page behaves like that shopkeeper.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

lazy loading
Fetching a resource only when it is about to be needed, not when the page first loads.
poster frame
One still picture that stands in for the animation: before it loads, when it fails, or for reduced motion.
rootMargin
An IntersectionObserver setting that makes an element count as visible a little before it really is.
fallback
What the page shows when the real thing cannot load. It should take the same space.
layout shift
Content jumping because something loaded late and took space. Reserving the size prevents it.
dynamic import
import(): loads a module when the line runs, as a promise. It rejects if the download fails.

Step by step

01Reserve the space

Give the canvas a fixed width and height in CSS (120px here). Before the file arrives the box is empty but the page does not move later.

02Wait until it is near the screen

An IntersectionObserver fires when the canvas comes within the margin. Only then create the player. The first example proves it by counting the resource requests before and after.

03Draw the poster, then decide

On load: if motion is allowed play the greeting, else setFrame(30) and stop. The second example does the three cases in a row: motion on, motion off, file missing.

04Plan for the WASM failing

Point the player at a WASM file that does not exist (third example). Read the real console lines. The fix is not in the player: it is your loadError handler showing the fallback.

05Measure on a real phone

Our 0.2 ms per frame is a laptop in a test browser. Open your page on the cheapest device you support, with DevTools' network throttling set to a slow connection, and check both the poster and the fallback.

Try it yourself

  1. 1

    Scroll less

    In the lazy example change scrollTo(0, 250) to scrollTo(0, 20). Only the first line prints: the canvas never gets near the visible part of the box, so the player is never created and nothing was downloaded. That is the saving.

  2. 2

    Switch on Reduced motion

    Under the poster example turn on Reduced motion and press Run. The last line says motion reduced. The case that the page's own setting decides now matches the second mascot: one still frame.

Code & diagrams

Create the player only when it is seen

The box scrolls by itself after 300 ms (standing in for the visitor). In this preview the library is imported at the top, because dynamic `import()` has no import map in a page without a static import. The WASM and the file are still fetched only when `new DotLottie` runs; the counts come from `performance.getEntriesByType("resource")`.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

at the start: player not created, wasm and lottie requests: 0
canvas seen: creating the player now
loaded: poster frame drawn, wasm and lottie requests: 2
Poster, reduced motion and a missing file

Three mascots: playing, a still poster frame (the reduced-motion answer), and a plain shape for a file that does not exist. The last line reads this page's own setting: switch on Reduced motion under the preview and run it again to see it say `reduced`, with the first mascot still playing because the example forces each case.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

motion allowed: playing greet
reduced motion: still poster frame
Failed to load animation data from URL: /data/anim/nope.lottie. Error: Failed to fetch animation data from URL: /data/anim/nope.lottie. 404: Not Found
file missing: fallback shape shown
this page's own setting: motion allowed
The player's WASM cannot load

`DotLottie.setWasmUrl()` must run before the first player: it only affects a WASM load that has not started. The `[warn]` and `[error]` lines are the library's own, in order; then your `loadError` handler runs and the page tells the visitor what to expect.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:
 TypeError: Failed to execute 'compile' on 'WebAssembly': HTTP status code is not ok
Primary WASM load failed from /data/anim/nope.wasm: WebAssembly.instantiate(): expected magic word 00 61 73 6d, found 6e 6f 74 20 @+0
Retrying WASM load with buffered instantiation
Buffered WASM load from /data/anim/nope.wasm failed: fetch /data/anim/nope.wasm responded with 404 Not Found
Primary WASM URL failed: WebAssembly.instantiate(): expected magic word 00 61 73 6d, found 6e 6f 74 20 @+0
Buffered fallback failed: fetch /data/anim/nope.wasm responded with 404 Not Found
[dotlottie-web] Initialization failed: Error: WASM loading failed from all sources.
loadError: Error: Failed to load wasm module: Error: WASM loading failed from all sources.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the `loadError` handler

In the poster example delete the whole p.addEventListener("loadError", …) block.

terminal
$ Press Run
── what you'll see ──
The first two lines print, then only the library's own `[error] Failed to load animation data from URL: /data/anim/nope.lottie …` line. The `file missing` line and the last line never print (the promise for the missing file never resolves), and the row has an empty 70 px canvas where the fallback shape should be.

Myth vs fact

Myth

The poster is only for slow networks.

Fact

It is also the reduced-motion picture and the failure picture. One frame serves all three, so draw it deliberately.

Myth

freezeOnOffscreen makes lazy loading unnecessary.

Fact

It stops drawing. It does not stop the download. The 530 KB still comes at page load unless you create the player late.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The library writes its own failure text to the console ([error] Failed to load animation data from URL…). Your monitoring (Topic 18.5) can listen to loadError to count failures instead of scraping console text.

  • ▸

    Never wait for the animation to show the page. The mascot is decoration: nothing may depend on its load or complete, and a focus or click handler must work with it absent.

  • ▸

    Preload with care: <link rel="preload" as="fetch"> for a file you will surely need soon can help, but a preloaded file that is never used costs the same bytes as any other.

Remember this

  1. 1

    The problem this solves: a player of about 530 KB over the network (Topic 16.1) loaded for a mascot at the bottom of the page slows everything above it. And any of three things can fail: the file (404 or cut off), the player's WASM, or the visitor's wish for less motion. A page that handles only the happy path leaves a hole or a spinner.

  2. 2

    Load lazily. Watch the canvas with an IntersectionObserver (a rootMargin makes it count as visible a little early; the build uses 100 px). Only then import the library and create the player. In the example below, before the scroll there were 0 requests for .wasm or .lottie files, and after it 2. In your project use await import("@lottiefiles/dotlottie-web") inside the callback, so the 165 KB of JavaScript waits too; Vite splits it into its own file.

  3. 3

    Show something before the player is ready. The canvas is empty until load. Reserve its size in CSS so the layout does not jump, and give it a background or a poster picture. After load, call setFrame(n) to draw one still frame; this is also the reduced-motion picture. For our mascot frame 30 is the settled pose with the lamp up and the beam on (we looked at it).

  4. 4

    Handle every failure with the same fallback. loadError covers a missing file (404), a file that is not Lottie, and a player WASM that cannot load. In our run a WASM URL that returned 404 produced three [warn] lines, then three [error] lines and finally loadError: Error: Failed to load wasm module: Error: WASM loading failed from all sources. The fallback (a plain shape, a still image, or nothing) must be the same size so the layout stays put. Wrap the dynamic import() in try/catch too: offline or blocked, it rejects.

  5. 5

    Stop when nobody is looking. renderConfig.freezeOnOffscreen is on by default. Our own observer adds control: it pauses a half-finished greeting when the canvas leaves the screen and resumes it on return. A hidden tab pauses requestAnimationFrame, which the player's loop uses; we did not test what the mascot does when the tab returns, so the build does not depend on it.

  6. 6

    Reduced motion means a still, not nothing. Check the setting after load: if motion is off, draw the poster frame and do not play. Also listen for changes (onMotionChange, Topic 7.3), because a visitor can switch it while the page is open: the build pauses and shows the poster again.

  7. 7

    Measure size and cost. Size: wc -c for the file, and the network panel for the player (about 165 KB and 1.24 MB, or 33 KB and 498 KB gzipped, in our package). CPU: we timed 120 setFrame() calls for the 200 × 200 mascot and got 0.26, 0.22 and 0.19 ms per frame in three rounds, headless Chromium on this laptop. That is a measurement of drawing one frame, not of a phone: Lena's cheap Android needs its own test.

Explain it without notes

01

Why can lazy loading matter more for a Lottie than for a GIF?

02

What does the player report when its WASM fails to load, and what should the page do?

03

What should a reduced-motion visitor see for a mascot, and why not nothing?

Practice

01

Write an IntersectionObserver callback that creates the player once, when the canvas is near the screen, and then stops observing.

02

Write the loadError handler that replaces the canvas by a <div class="fallback"> of the same size.

Done when you can

  • You can create a player only when its canvas is near the screen

  • You can draw a poster frame and use it for load, failure and reduced motion

  • You handle loadError and a failing import() with the same fallback

  • You can measure the file, the player and the per-frame cost, and say where the measurement was made