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, or DT, 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:

  1. Isolation: When a reader visits build_module.html, the browser loads exactly one R Wasm instance.
  2. 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).
  3. Lightweight Index: The landing page docs/demos/index.html contains 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 with connect_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 seaborn if you can generate plots with matplotlib directly).
  • 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.

This site uses Just the Docs, a documentation theme for Jekyll.