Shinylive Performance Tradeoffs
The trade-offs of using WebAssembly-powered Shinylive apps directly in the browser are indeed centered around RAM consumption and CPU initialization overhead.
Here is a breakdown of what happens under the hood, the trade-offs, and how our restructuring helps mitigate performance hits on smaller browsers.
1. The Core Performance Trade-Off
| Factor | Client-Side (Shinylive / Wasm) | Server-Side (Shinyapps.io / Posit Connect) |
|---|---|---|
| Initial Load | Slow (Downloads 20–40MB Wasm runtimes + packages). | Fast (Standard HTML/JS payload). |
| Browser RAM | High (~150MB–300MB per running app instance). | Low (Under ~20MB; server does all the heavy lifting). |
| Server Cost | Zero ($0 static hosting via GitHub Pages). | Variable (Requires running R/Python server). |
| Scaling | Unlimited (Scale to millions of users instantly). | Limited (Restricted by server core/memory limits). |
2. What triggers high browser memory usage?
- Multiple WebAssembly Workers: Each
{shinylive-r}or{shinylive-python}block on a page initializes a separate virtual R (WebR) or Python (Pyodide) session in the browser. If a single page has 3 separate embedded code blocks, the browser spins up 3 distinct virtual runtimes, easily eating 600MB–1GB of RAM. - Heavy Package Imports: When you call packages like
ggplot2,dplyr, orDT, WebAssembly must download, parse, and load their binary files into the browser’s memory. - Service Worker Caching: Shinylive uses Service Workers to cache the compiled assets locally. This makes subsequent loads instantaneous in terms of download speeds, but it does not reduce the RAM required to run the virtual environment once loaded.
3. How our Restructuring Solved the Monolithic Memory Leak
By splitting the single demo_gallery.qmd page into a demos/ subdirectory containing separate files, we implemented the single most important performance optimization:
- Isolation: When a reader visits
build_module.html, the browser loads exactly one R Wasm instance. - Garbage Collection: When the user clicks the “Back” link and navigates to the landing page or another demo, the browser automatically terminates the old Wasm thread and releases the memory (~250MB RAM freed immediately).
- Lightweight Index: The landing page
docs/demos/index.htmlcontains only static cards generated by Quarto. It consumes virtually zero extra memory and loads instantly on mobile browsers.
4. Tips for Optimizing Future Demos
As you expand the gallery, keep these guidelines in mind to protect low-end browsers:
- Avoid Multi-App Pages: Never embed multiple separate
{shinylive}chunks on the same page. If you want to show different modules side-by-side or stacked, combine them into one single multi-module Shiny app (like we did withconnect_modules.qmd). A single app shares one WebR worker thread. - Keep Package Lists Lean: Avoid loading unnecessary packages. In Python, import only what is necessary (e.g., skip
seabornif you can generate plots withmatplotlibdirectly). - Provide Static Fallbacks (If needed): For extremely heavy apps, instead of embedding a live Shinylive iframe directly on page load, you can link to it as a separate page, or display a static screenshot of the app with a “Launch Live Interactive Version” link so the Wasm worker only spins up when explicitly requested.