Development notes
Should hidden tabs, reduced motion, resize, and pagehide be handled together?
Using the currently mounted Canvas engineering grid to distinguish pausing, static redraws, page-exit cleanup, and BFCache restoration, with the limits of local checks for tab switching, media preferences, and history navigation.
- Canvas
- Animation lifecycle
- reduced-motion
- BFCache
A background animation working when a page first opens does not mean it will still behave correctly after switching tabs, resizing, or navigating back.
A hidden tab needs to stop continuous updates. A preference for reduced motion calls for a static image. A resized window needs the Canvas reconfigured, and leaving the page requires releasing the controls created for that lifecycle. These events all affect the same animation loop, but calling all of them “stop” is not enough.
For this note, I traced the initialization and cleanup paths of the engineering grid background that is actually mounted, then checked its behavior in different states with a local browser. The earlier flow-line article retains the implementation and measurements from that time; this note does not treat an old background component still in the project as the effect currently running on the page.
Distinguish pausing, a static state, and disposal first
The current grid controller tracks isPaused, prefersReducedMotion, disposed, and the next animationFrameId separately. They answer different questions:
| State | What needs to be retained | Main action |
|---|---|---|
| Visible, with motion allowed | Data and drawing controls for the current animation lifecycle | Update and draw on each rAF, then schedule the next one |
| Hidden tab | The existing controller and data | Cancel further animation scheduling and wait for visibility to change |
| Reduced motion | A readable static background | Stop continuous animation and redraw when needed |
| Window resized | The motion preference and current controller | Rebuild the canvas dimensions and grid, then draw according to the current state |
| Leaving the page | Controls must be recreated by the outer layer if the background is to resume | Remove this lifecycle’s listeners, cancel rAF, and release the context reference |
If a hidden tab is treated as permanent disposal, there may be no controller left to resume work when the user returns. Conversely, merely pausing the animation when leaving the page does not clean up listeners and references.
I therefore decide what each state means before wiring up events, rather than routing every event into the same stop function.
Pause while hidden and reset the time baseline on return
MDN’s Page Visibility API documentation explains that changes in tab visibility can be detected through visibilitychange and the document’s state. Most browsers also stop providing rAF callbacks to background tabs.[2]
The browser’s own scheduling policy does not replace the application’s state handling. The current controller cancels the scheduled rAF when the page becomes hidden. Once it is visible again, the motion preference determines whether to draw a static background once or resume animation.
On resuming, it also resets previousRenderTime, rather than treating the entire time spent away from the page as a distance the next frame needs to catch up on. Normal rendering separately caps the time delta used for a single update. This is a visual-update trade-off, not a simulation that precisely tracks all elapsed time in the background.
In the local test, I opened another tab and switched to it. I first confirmed that document.hidden on the original page had actually become true, then compared the counts. Neither callback nor draw counts increased during that observation period; drawing resumed after returning to the original tab.
Manually dispatching a visibilitychange event without checking the actual document state does not count as this kind of tab-switching verification. I also did not change other settings while the page was hidden in this test, so stable counts cannot be taken to mean that no combination of events could ever trigger a redraw.
Reduced motion does not mean running the same animation more slowly
prefers-reduced-motion expresses the user’s preference for less nonessential motion; it does not rewrite a Canvas render loop for you.[3]
With reduced motion enabled, the current grid cancels continuous rAF scheduling and draws a static grid, without advancing the animated highlights that sweep across the nodes. This is a different decision from limiting the normal animation to a lower FPS.
The controller reads the media query at initialization and also listens for subsequent change events. There is therefore a transition path when the preference changes after the page has opened. When motion is allowed again, animation restarts if the page is visible and no frame is already scheduled.
For this test, I used the browser’s media preference emulation to switch to reduce. After the change event had been handled, I read the counts twice to confirm that continuous updates had stopped. After switching back to no-preference, draw counts increased again. This demonstrates behavior under that browser emulation, not tests of every operating system’s settings interface.
For the same reason, I would not use equality between total callback and draw counts in reduced-motion mode to judge whether normal mode is dropping frames. Events directly trigger some static redraws, which do not require a new rAF callback. Counts need to be compared as increments within the same state.
A static background still needs resize handling
Stopping continuous animation does not mean the canvas can keep its initial dimensions forever.
The current implementation configures the Canvas using the window dimensions and device pixel ratio, and rebuilds the grid data and static layer at the same time. In animated mode, the next render draws the result. With reduced motion enabled and the page visible, resize directly triggers one redraw so that the old dimensions do not remain on screen.
In this test, while in the reduce state, I changed the emulated viewport from 1280 × 900 to 900 × 700 with the device pixel ratio set to 1. The canvas dimensions updated accordingly, draw counts increased once, and there were no further increases during the subsequent observation period.
This checks the relationship between dimensions and scheduling, not mobile appearance, every pixel ratio, or performance. Treating an unmoving image as proof that reduced-motion handling is complete would miss the responsibility to produce the correct static output after a window resize.
Pair pagehide cleanup with BFCache restoration
The current background component holds the cleanup function returned by the controller. It calls that function on pagehide; on pageshow with persisted, it first cleans up the old controls and then initializes the background again.
This cleanup removes the resize, visibility, and media preference listeners from that lifecycle, cancels pending rAF, and releases the Canvas context reference. disposed also prevents repeated cleanup calls from doing the same work again. The outer listeners for page exit and restoration remain responsible for connecting the next controller lifecycle.
MDN lists browser forward and back navigation, as well as restoring a page from BFCache, among the situations covered by pageshow.[5] Navigating back does not guarantee a BFCache hit, though; pressing Back alone is not enough to claim that cache restoration was tested.
In this test, I actually left the About page and returned through browser history. I recorded pageshow.persisted === true, and background draw counts began increasing again. This was an observation of a genuine BFCache hit, not a manually dispatched event with the same name.
Canvas controls and existing page buttons do not need to follow the same cleanup policy. The About page retains its main interaction listeners in the persisted branch of the page-exit event, while the background rebuilds its own drawing controls. After that return, the avatar could still be opened, closed with Escape, and have focus restored. I did not force a full-page reload to restore the background.
These results still do not prove the absence of duplicate listeners across every number of return navigations and every browser. MDN also warns that pagehide does not fire reliably in every mobile page-exit scenario.[4] For this decorative background, it is one part of lifecycle handling, not a reliable guarantee of data preservation or remote transmission.
Keep the observation conditions alongside each state
This round of testing ran locally in a headless Edge browser. The main results are below. Media preferences and viewport dimensions were emulated by the browser; for tab visibility and BFCache, I read the native state before assessing the result.
| Check | Observation in this test |
|---|---|
| Visible, with motion allowed | During a sample of about 600 milliseconds, callback and draw counts each increased by 37 |
Switched to reduce and waited for the event to finish |
Neither count increased over the following approximately 500 milliseconds |
Resized the viewport while in reduce |
The canvas updated and redrew once, then remained static |
| Allowed motion again | Draw counts increased again |
| Switched to another tab and confirmed that the original page was hidden | Callback and draw counts stayed unchanged during that observation period |
| Switched back to the original tab | Drawing continued |
| Returned through native browser history with a BFCache hit | Received a persisted restoration event; the background resumed and avatar interactions remained usable |
This set of tests did not measure power consumption, GPU cost, every device refresh rate, or long-term stability. It also did not cover every permutation of state transitions. Observing that static mode stops updates does not establish how many resources it saves.
The earlier background flow-line note covers update frequency in normal mode. This note addresses a different question: when animation should continue, when it should redraw only once, and who takes control again after leaving the page. Keeping it moving is not a reason to treat every state as a normal, visible page.
Sources
[2] https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API [3] https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion [4] https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event [5] https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event