Pre-Rendering¶
Pre-rendering executes Panel code at build time and embeds the resulting static HTML inside each <panel-live> element. When users load the page they see the output instantly — before Pyodide has even started downloading.
Why Pre-Render?¶
Without pre-rendering, every <panel-live> element starts blank and shows a loading spinner while Pyodide (300-500 MB) downloads, initializes, and executes the code. This can take 5-15 seconds on a fast connection. Pre-rendering eliminates that wait for the initial view.
Benefits:
- Instant visual output — content appears on page load, not after Pyodide boots.
- SEO-friendly — search engines index the static HTML.
- Fallback — browsers without WebAssembly still see the output.
- Perceived performance — users see results immediately, even if interactivity takes a few seconds to become available.
How It Works¶
Build-Time Pipeline¶
- Hash the code. The full source (including any
setup_code) is hashed with SHA-256 to produce a cache key. - Check the cache. If a file named
{hash}.jsonexists in the cache directory, its contents are returned immediately — no subprocess is spawned. - Spawn a subprocess. On cache miss, a
multiprocessing.Process(using thespawncontext) executes the code viapanel.io.mime_render.exec_with_return(). - Serialize to JSON. The resulting Panel/Bokeh object is serialized to JSON via
bokeh.embed.standalone.standalone_docs_json_and_render_items(). - Cache the result. The JSON is written to the cache directory with
portalockerfile locking (falling back to plain writes whenportalockeris not installed). - Embed in HTML. The JSON is wrapped in a
<script type="application/json" class="panel-live-prerender">tag inside the<panel-live>element.
Page-Load Pipeline¶
- The
<panel-live>custom element finds the embedded<script class="panel-live-prerender">child. - It parses the JSON and calls Bokeh's
embed_items()to render the static output. - The status overlay is hidden, and the output is visible immediately.
- When Pyodide finishes loading (in the background), the element transitions to interactive mode — the pre-rendered output is replaced by the live output.
Transition to Interactive¶
- When
auto-run="true"(the default), the element automatically re-executes the code once Pyodide is ready and replaces the static output with the live version. - When
auto-run="false", the static output persists until the user clicks "Run". This is recommended for documentation pages with many examples.
Caching¶
Pre-rendered output is cached in a directory (default .panel-live/) using content-hash filenames:
- Cache hit — if the code hasn't changed, the cached JSON is returned without spawning a subprocess.
- Cache invalidation — changing the code (or
setup_code) produces a different hash and triggers re-rendering. - File locking —
portalockeris used for file-level locking to support parallel builds. Whenportalockeris not installed, plain file writes are used (safe for serial builds).
Add .panel-live/ to .gitignore:
Framework Integration¶
Pre-rendering is available across all three supported documentation frameworks. The simplest approach is the per-fence attribute — add pre-render="true" to individual fences or directives without any configuration changes.
| Framework | Per-Fence On | Site-Wide | Per-Fence Off |
|---|---|---|---|
| MkDocs | pre-render="true" attribute |
prerender_formatter or configure() hook |
pre-render="false" attribute |
| Sphinx | :pre-render: true option |
panel_live_conf = {"pre_render": True} |
:pre-render: false option |
| Quarto | python -m panel_live pre-render via Lua io.popen() |
(call CLI for all fences) | (call CLI selectively) |
| CLI | python -m panel_live pre-render CODE |
N/A | N/A |
See the Pre-Rendering How-to Guide for live demos of all combinations.
MkDocs¶
Per-fence (simplest — no config change needed):
Site-wide via formatter — point superfences at prerender_formatter:
custom_fences = [
{ name = "panel", class = "panel-live", validator = "panel_live.fences.validator", format = "panel_live.fences.prerender_formatter" }
]
Site-wide via hook (advanced — custom cache_dir, setup_code, etc.):
# docs/hooks/panel_live_prerender.py
from panel_live.fences import configure
def on_config(config):
configure(pre_render=True, cache_dir=".panel-live")
return config
Register the hook in your MkDocs config:
Sphinx¶
Per-directive (simplest):
Site-wide — pre-rendering is enabled by default when using the panel_live.sphinx extension:
# conf.py
extensions = ["panel_live.sphinx"]
panel_live_conf = {
"pre_render": True, # default
"setup_code": "", # prepended to all directives
}
See the Sphinx Integration Guide for full configuration.
CLI¶
The CLI is useful for Quarto integration and scripting:
# Inline code
python -m panel_live pre-render "import panel as pn; pn.panel('hello').servable()"
# From a file
python -m panel_live pre-render --file examples/app.py
# With options
python -m panel_live pre-render --file app.py --cache-dir .cache --timeout 60
The JSON output is printed to stdout. Quarto's Lua filter can call this via io.popen().
Build Requirements¶
Pre-rendering executes Panel code in a subprocess, so the build environment must have:
- Panel and Bokeh installed (matching the versions configured for the frontend)
- Any packages imported by the code (e.g.
hvplot,numpy,pandas)
The subprocess uses panel.io.mime_render.exec_with_return(), which requires a working Panel installation.
Skipping Pre-Render¶
Some code cannot be pre-rendered (e.g. code that requires browser APIs or user interaction). Use pre-render="false" to exclude specific blocks:
Sphinx:
.. panel-live::
:pre-render: false
import panel as pn
pn.panel("This won't be pre-rendered").servable()
MkDocs:
```{.panel pre-render="false"}
import panel as pn
pn.panel("This won't be pre-rendered").servable()
```
Limitations¶
- No browser APIs — code that accesses
window,document, or other browser globals will fail in the subprocess. - Only initial state — pre-rendering captures the initial render of the Panel app. Dynamic behavior (callbacks, streaming) is only available after Pyodide loads.
- Build time — each uncached directive spawns a subprocess. Large documentation sites may have longer build times. The cache mitigates this for rebuilds.
- Memory — each subprocess loads Panel/Bokeh. Parallel builds with many directives may use significant memory.