Precompiled vignettes and articles with rawvignette
Source:vignettes/rawvignette.Rmd
rawvignette.RmdThe problem
By default, R rebuilds your vignettes every time the package is built
– during R CMD check, on CRAN, on GitHub Actions, and when
pkgdown generates your site. Usually that is a good thing: it keeps the
rendered output in sync with the packge source code.
But when a vignette or pkgdown article does heavy work (a long or memory-intensive computation, a call to a rate-limited API, a large dataset) rebuilding it everywhere, every time, can become annoying or impossible.
Here is the kind of chunk that causes the trouble. Imagine the
Sys.sleep() below is an intensive simulation that depends
on a large input dataset and takes two hours to run:
expensive_result <- {
Sys.sleep(2) # stand-in for genuinely slow work
summary(1:1000)
}
expensive_result
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 1.0 250.8 500.5 500.5 750.2 1000.0If that chunk lives in an ordinary vignette, every build server needs access to the input data and has to run a two-hour job each time the vignette is built.
The idea
The alternative enabled by rawvignette is to build the
vignette once, on your machine, and ship the already-computed
result.
The workflow splits a vignette into two stages:
- A source in
vignettes-raw/, with live, executable code. You edit and run this locally, whenever you choose. - A staged
.Rmdinvignettes/, produced by knitting the source. Its code has already been executed and its output inlined, so there is nothing expensive left to run.
Downstream tools only ever see the staged form.
R CMD check, CRAN, and pkgdown re-knit it, but since its
chunks are already evaluated, that re-knit is instant.
This is the well-established “precompiled vignette” idea (see the rOpenSci
tech note); rawvignette just adds tooling around it –
scaffolding, figure handling, .Rbuildignore management, and
a freshness check – so you do not have to script those by hand.
Walking through the workflow
The functions expect to run from a package root. To keep this vignette self-contained and runnable, the examples below operate in a disposable throwaway package created in a temporary directory (set up invisibly when this vignette starts). In your own work you would skip that step and simply run the workflow from your package’s top level.
1. Scaffold a source
use_raw_vignette() sets up a skeleton for a new
pre-compiled vignette or article. You can also use it to migrate an
existing standard vignette or article to the rawvignette
workflow. The function wires up the supporting infrastructure (the
vignettes-raw/ directory, a .Rbuildignore
entry, and a vignettes/figures/ directory for vignettes),
and either creates a new vignette/article .Rmd file for you
to edit, or moves your existing .Rmd into the new
directory.
use_raw_vignette("intro", title = "Introduction")
#> Created vignettes-raw/intro.Rmd
#> Added `^vignettes-raw$` to .Rbuildignore
#>
#> Next steps:
#> 1. Edit vignettes-raw/intro.Rmd
#> 2. Run: `rawvignette::precompile_raw_vignettes()`
#> 3. Commit vignettes-raw/intro.Rmd, vignettes/intro.Rmd, and any new figures.
#> 4. [OPTIONAL] Run `rawvignette::use_raw_vignette_hook()` to configure a
#> pre-commit check for stale vignettes, so you don't forget to precompile.That leaves you with a skeleton source to edit:
list.files("vignettes-raw")
#> [1] "intro.Rmd"In real use you would now open vignettes-raw/intro.Rmd
and write your vignette there, iterating and running code the same way
you would for a normal vignette. For the demo, let’s drop in a chunk
that does some (pretend-expensive) work:
writeLines(c(
"---",
'title: "Introduction"',
"output: rmarkdown::html_vignette",
"vignette: >",
" %\\VignetteIndexEntry{Introduction}",
" %\\VignetteEngine{knitr::rmarkdown}",
" %\\VignetteEncoding{UTF-8}",
"---",
"",
"```{r}",
"# Pretend this is slow:",
"nrow(mtcars)",
"```"
), "vignettes-raw/intro.Rmd")2. Precompile
precompile_raw_vignettes() knits every source under
vignettes-raw/ to its counterpart in
vignettes/, running the code once and inlining the results.
It also injects a “do not edit by hand” notice identifying the
source.
precompile_raw_vignettes()
#> Knitting vignettes-raw/intro.Rmd -> vignettes/intro.RmdThe shipped vignette now exists, with the computed output baked in:
list.files("vignettes")
#> [1] "figures" "intro.Rmd"
out <- readLines("vignettes/intro.Rmd")
cat(head(out, 25), sep = "\n")#> ---
#> title: "Introduction"
#> output: rmarkdown::html_vignette
#> vignette: >
#> %\VignetteIndexEntry{Introduction}
#> %\VignetteEngine{knitr::rmarkdown}
#> %\VignetteEncoding{UTF-8}
#> ---
#>
#> <!--
#> =====================================================================
#> WARNING: THIS FILE IS GENERATED by rawvignette. Do not edit by hand.
#> Source: vignettes-raw/intro.Rmd
#> Regenerate with: rawvignette::precompile_raw_vignettes()
#> =====================================================================
#> -->
#>
#>
#> ``` r
#> # Pretend this is slow:
#> nrow(mtcars)
#> #> [1] 32
#> ```
Notice the generated notice at the top: editing
vignettes/intro.Rmd directly would be a mistake, because
the next precompile overwrites it. The source of truth is
vignettes-raw/intro.Rmd.
3. Check freshness
Because the two files can drift – you edit the source, then forget to
re-precompile – you can use check_raw_vignettes() to
compares their file modification times and flags any stale output.
check_raw_vignettes()
#> All precompiled vignettes are fresh (by mtime; see ?check_raw_vignettes for caveats).Right after precompiling, everything is fresh. If you now edit the source without re-precompiling, the check notices:
# Simulate editing the source after the output was built.
Sys.setFileTime("vignettes-raw/intro.Rmd", Sys.time() + 5)
check_raw_vignettes()
#> Stale precompiled vignette:
#> - intro
#> Run rawvignette::precompile_raw_vignettes() to regenerate.This is a heuristic, not a guarantee. It catches the common slip – an
un-precompiled edit – but cannot detect staleness caused by changes to
your package code, data files, or dependency versions, since those do
not touch the source .Rmd. The only robust guarantee is to
re-run precompile_raw_vignettes() before a release.
To catch the forgot-to-precompile slip automatically, you can install
a git pre-commit hook that runs the freshness check and blocks a commit
when an output is stale, using use_raw_vignette_hook(). If
you already have a pre-commit hook (for example, the one
usethis::use_readme_rmd() installs), pass
on_collision = "append" to add the check alongside it
instead of replacing it. The hook is a local convenience: it lives in
.git/hooks/, so it is not version-controlled and must be
re-installed per clone, and like the freshness check it compares
modification times only. See ?use_raw_vignette_hook for
details.
Articles
pkgdown also displays articles: vignette-like
documents that appear on your website but do not ship with the package
(handy for content that is large, depends on unavailable resources, or
simply does not belong on CRAN). rawvignette precompiles
articles the same way – the main difference is where they live.
Pass an articles/ prefix, and the source goes to
vignettes-raw/articles/, the output to
vignettes/articles/, and an extra
.Rbuildignore entry keeps the whole
vignettes/articles/ subtree (rendered article plus its
figures) out of the package tarball:
use_raw_vignette("articles/benchmark", title = "Benchmark")This creates an Rmd. with slightly different header
parameters, appropriate for a pkgdown article. Everything else –
editing, precompiling, the freshness check – works identically. There is
no separate flag to set; the articles/ location is the
single signal that a document is an article rather than a vignette.
When to use rawvignette
Precompilation has a real cost: your documentation no longer rebuilds automatically when the package changes, so you take on the discipline of re-precompiling when relevant inputs change. That trade is worth it for genuinely expensive vignettes, and not worth it for cheap ones. Precompile the documents that are slow, flaky, or resource-hungry; leave the rest as ordinary vignettes.