Skip to main content

22  webR

Published

2026-11-03

CautionCaution

The contents for this section are being developed. Thank you for your patience.

  • webR is R compiled to WebAssembly, so it runs inside a web browser.

  • shinylive bundles a Shiny app and webR into static files that any web host can serve.

  • Every package the app uses needs a WebAssembly binary, including the app-package itself.

  • R-universe builds a WebAssembly binary of our app-package each time we push to GitHub.

Every deployment we’ve covered so far needs a server running R. Posit Connect Cloud runs R for us (Chapter 19). Docker packages R into a container (Chapter 20), and GitHub Actions builds and ships it (Chapter 21). In this chapter we’ll remove the server entirely and use webR and shinylive to run our app-package in the user’s browser.

I’ve created the shinypak R package in an effort to make each section accessible and easy to follow. Install shinypak using pak (or remotes):

install.packages('pak')
pak::pak('mjfrigaard/shinypak')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '^22')

Launch apps with launch()

launch(app = '22_webr')

Download apps with get_app()

get_app(app = '22_webr')

22.1 What is webR?

webR is a version of R that runs in a web browser. It’s compiled to WebAssembly, a binary format that modern browsers run at near native speed. When a browser loads webR, the R session lives on the user’s machine, not on a server.

shinylive builds on webR. It packages a Shiny app, webR, and the app’s R packages into a folder of static files. A static file host is just a web server that hands out files without running any code (GitHub Pages and Netlify are common examples).

The diagram below compares a server-based deployment with a shinylive deployment:

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"16px", "darkMode":false}}}%%

flowchart TD
    subgraph Server["<strong>Server-based Shiny</strong>"]
        B1(["Browser"]) <-->|"inputs and outputs"| R1("R process on a server<br>runs <code>launch_app()</code>")
    end
    subgraph Live["<strong>shinylive</strong>"]
        H1["Static file host"] -->|"i. download app files"| B2(["Browser"])
        B2 -->|"ii. start"| W1("webR inside the browser<br>runs the app")
    end

    style Server fill:#FFFFFF,color:#000000,stroke:#333,stroke-width:1px,rx:10,ry:10
    style Live fill:#FFFFFF,color:#000000,stroke:#333,stroke-width:1px,rx:10,ry:10
    style R1 fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
    style W1 fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5

Server-based Shiny versus shinylive

In a server-based deployment, every click travels to the server and every plot travels back. In a shinylive deployment, the host only delivers the files. After the first download, the app runs without contacting a server at all.

22.1.1 Trade-offs

shinylive removes the server, but the browser takes on the server’s job. I’ve found these trade-offs matter most for app-packages:

  • First load: the browser downloads webR and every package before the app appears, so the first visit takes longer than a server-based app.

  • Visible code: all the app’s code and data are sent to the browser. Never put credentials, API keys, or private data in a shinylive app.

  • No system tools: webR can’t call programs installed on a computer. Features that rely on the Quarto CLI or system() calls won’t work.

  • No downloads from R: webR can’t download files from inside R. sass::font_google() downloads font files by default, so a bslib theme that uses it fails with an error about the curl package. Use sass::font_google("Ubuntu", local = FALSE) so the browser loads the font instead.

  • Temporary files: webR writes files to a virtual file system in browser memory. Log files written with logger disappear when the user closes the tab.

22.2 Which packages run in webR?

webR can’t install packages from source. Every package an app uses needs a precompiled WebAssembly binary. The webR project hosts thousands of these binaries in a CRAN-like repository at https://repo.r-wasm.org.

Before we export anything, we should confirm that each package in our Imports field has a binary. The code below reads the DESCRIPTION file and compares it to the packages in the webR repository:

wasm_pkgs <- available.packages(
  contriburl = "https://repo.r-wasm.org/bin/emscripten/contrib/4.6"
)
base_pkgs <- rownames(installed.packages(priority = "base"))
imports <- read.dcf("DESCRIPTION", fields = "Imports")
imports <- trimws(strsplit(imports, ",")[[1]])
imports <- imports[imports != ""]
setdiff(imports, c(rownames(wasm_pkgs), base_pkgs))

Base packages (like tools) ship with webR, so we remove them before we compare. An empty result means every dependency has a binary:

character(0)

The 4.6 in the URL refers to the version of R that webR is built on. Check the webR documentation for the current version if this URL stops working.

22.3 A WebAssembly binary for sap

Our dependencies are covered, but sap itself isn’t on CRAN, so it has no binary in the webR repository. We need a service that builds one for us. R-universe is a free service from rOpenSci that builds R packages from GitHub. It builds a WebAssembly binary of every package it hosts.

To set up a personal universe:

  1. Create a GitHub repository named <username>.r-universe.dev (for this book, I used mjfrigaard.r-universe.dev).

  2. Add a packages.json file that lists each package and the repository it comes from.

  3. Install the R-universe GitHub app on your account.

The packages.json file below adds sap to the universe. The branch field tells R-universe to build from the 22_webr branch instead of the default branch:

[
  {
    "package": "sap",
    "url": "https://github.com/mjfrigaard/sap",
    "branch": "22_webr"
  }
]

22.3.1 Installing the R-universe GitHub app

The packages.json file only lists our packages. Nothing builds until we install the R-universe GitHub app, which tells R-universe to create the universe and watch the repositories in it.

  1. Go to https://github.com/apps/r-universe/installations/new.

  2. Choose the personal account (or organization) that owns the <username>.r-universe.dev repository.

  3. Under “for these repositories,” choose All repositories or Only select repositories. If you pick select repositories, include the <username>.r-universe.dev repository.

  4. Review the permissions (read access to metadata, and read and write access to commit statuses), then click Install.

Installing the R-universe GitHub app

Installing the R-universe GitHub app

The app needs read access to the package repositories as well. Public repositories like sap are readable either way, but R-universe only posts build status checks to repositories the app is installed on. Add the package repository to the selection if you want those checks on your commits.

Installed R-universe GitHub app

Installed R-universe GitHub app

After the app is installed, R-universe creates a build repository at github.com/r-universe/<username> (for this book, https://github.com/r-universe/mjfrigaard). The first build usually finishes within an hour. We can follow its progress in the Actions tab of that repository (https://github.com/r-universe/mjfrigaard/actions). When the build succeeds, the package appears at https://<username>.r-universe.dev.

On the first run, the Actions tab lists three workflows. Update universe and Cleanup package server set up the universe and sync it with packages.json, and Build package builds sap (including the WebAssembly binary):

R-universe workflows building sap

R-universe workflows building sap

The Installed GitHub Apps page in a repository’s settings only lists apps that are already installed on that repository. If R-universe isn’t listed there, the app hasn’t been installed yet: use the installation link above (not the repository settings) to install it.

22.3.2 Installing sap from R-universe

R-universe rebuilds the package (and its WebAssembly binary) each time we push to that branch. When the build finishes, we install sap from the universe instead of from our local source files:

install.packages(
  "sap",
  repos = c("https://mjfrigaard.r-universe.dev", getOption("repos"))
)

22.3.3 Why install from R-universe?

Installing from R-universe matters because shinylive bundles the packages installed on our machine. The installed package records where it came from, and shinylive uses that record to download the matching WebAssembly binary.

22.4 The shinylive launcher

shinylive exports an app directory (a folder with an app.R file), not a package. We’ll add a small launcher app in inst/webr/ that loads sap and returns the application:

Launch app with the shinypak package:

launch('22_webr')
library(shiny)
library(sap)

shinyApp(
  ui = movies_ui(bslib = TRUE),
  server = movies_server
)

This file calls movies_ui() and movies_server() directly instead of launch_app(). launch_app() calls shiny::runApp(), which starts a server and blocks the R session. shinylive expects app.R to return a Shiny app object and handles the rest itself.

The library(sap) call also does a second job. shinylive scans app.R for library() calls to decide which packages to bundle. It finds sap, then bundles the binaries for sap and each of its dependencies.

22.5 Exporting the app

We’ll store the export code in dev/webr.R. The dev/ folder is already listed in .Rbuildignore, so the script won’t end up in the built package. shinylive::export() converts the launcher into a static site in _site/:

shinylive::export(appdir = "inst/webr", destdir = "_site")

The export downloads the shinylive assets the first time it runs, so the first export takes a few minutes. Later exports reuse the cached assets.

We can preview the site with httpuv::runStaticServer(). Opening _site/index.html directly in a browser won’t work, because webR needs the files served over HTTP:

httpuv::runStaticServer("_site")

The app should look the same as the version we launch with launch_app(app = "bslib"). We can confirm that R is running in the browser by opening the browser’s developer tools. The Network tab lists the webR and package files as they download.

We’ve also added _site/ to .gitignore. The exported site is a build artifact, so we rebuild it from source rather than tracking it in Git.

22.5.1 Publishing the site

Any static file host can serve the _site/ folder. GitHub Pages is a common choice. We can build and deploy the site with a workflow like the ones in Chapter 21. A repository has only one GitHub Pages site, and we’ll use the sap site for the package website in Chapter 24. If you want both, deploy the shinylive app from a separate repository.

The quarto-shinylive extension embeds shinylive apps directly in Quarto documents with a {shinylive-r} code block. This works well for small examples in documentation, but an app-package with modules and data is easier to maintain as an exported app.

Recap

webR and shinylive move our app-package from a server into the user’s browser. The trade-off is a slower first load and code that’s visible to every user.

TipRecap
  1. Check that each package in Imports has a WebAssembly binary at repo.r-wasm.org.

  2. Publish the app-package to R-universe so it gets a WebAssembly binary, then install it from there.

  3. Add a launcher app.R that returns a Shiny app object instead of calling runApp().

  4. Export the launcher with shinylive::export() and serve the static files from any web host.

In the next chapter we’ll package the same app as a desktop application with shinyelectron, which can also use shinylive to run without R installed.

Please open an issue on GitHub