Renderer architecture
This guide explains the internal architecture of JBrowse 2's renderer system, including the class hierarchy, RPC serialization, and how different renderer types handle features and transferables.
Overview
JBrowse 2 renderers run in a web worker to keep the main thread responsive. The renderer system handles:
- Serializing render arguments from client to worker
- Fetching features from data adapters
- Rendering to canvas or creating React elements
- Serializing results back to the client
- Transferring large data (like
ImageBitmap) efficiently
Renderer class hierarchy
RendererType (base)
└── ServerSideRendererType (RPC bridge)
├── FeatureRendererType (feature fetching + serialization)
│ ├── CircularChordRendererType
│ ├── ArcRenderer
│ ├── DivSequenceRenderer
│ ├── WiggleBaseRenderer (canvas)
│ ├── MultiVariantBaseRenderer (canvas)
│ └── BoxRendererType (layout management)
│ ├── LollipopRenderer
│ ├── SvgFeatureRenderer
│ ├── PileupRenderer (canvas)
│ └── CanvasFeatureRenderer (canvas)
├── HicRenderer
└── DotplotRenderer (canvas)
Base classes
RendererType
The base class that all renderers extend. Provides:
ReactComponent- The React component used for renderingconfigSchema- Configuration schema for the rendererrender(props)- Creates a React element (called by subclasses after processing)
ServerSideRendererType
Handles the RPC bridge between client and worker:
serializeArgsInClient()- Prepares arguments for sending to workerdeserializeArgsInWorker()- Reconstructs arguments in workerserializeResultsInWorker()- Prepares results for sending backdeserializeResultsInClient()- Reconstructs results on clientrenderInClient()- Calls RPC to trigger worker-side renderingrenderInWorker()- Orchestrates the worker-side render process
FeatureRendererType
Adds feature fetching and serialization:
getFeatures()- Fetches features from the data adapterfeaturePassesFilters()- Applies filter chain to featuresgetExpandedRegion()- Optionally expands the fetch region- Serializes/deserializes features as JSON for transport
BoxRendererType
Adds layout management for collision detection:
createLayoutInWorker()- Creates a layout session for positioning featuresserializeLayout()- Converts layout to JSONdeserializeLayoutInClient()- Reconstructs layout on client- Layout caching via
layoutSessionsfor performance
Two rendering patterns
Pattern 1: React-based renderers
For renderers that create React/SVG elements (e.g., SvgFeatureRenderer,
CircularChordRendererType):
class MyRenderer extends FeatureRendererType {
async render(renderArgs) {
// 1. Fetch features
const features = await this.getFeatures(renderArgs)
// 2. Pass to parent which creates React element
const result = await super.render({ ...renderArgs, features })
// 3. Return features for serialization
return { ...result, features }
}
}
The result goes through serializeResultsInWorker() which:
- Converts features Map to JSON array
- Strips React elements (can't be serialized)
Then deserializeResultsInClient():
- Reconstructs features from JSON
- Creates React element via
ReactComponent
Pattern 2: Canvas-based renderers with rpcResult
For renderers that draw to canvas and return ImageBitmap (e.g.,
PileupRenderer, WiggleBaseRenderer):
import { rpcResult } from '@jbrowse/core/util/librpc'
import { isImageBitmap } from '@jbrowse/core/util/offscreenCanvasPonyfill'
class MyCanvasRenderer extends FeatureRendererType {
async render(renderProps) {
// 1. Fetch features
const features = await this.getFeatures(renderProps)
// 2. Render to canvas
const { width, height, regions, bpPerPx } = renderProps
const region = regions[0]!
const canvasWidth = (region.end - region.start) / bpPerPx
const res = await renderToAbstractCanvas(
canvasWidth,
height,
renderProps,
ctx => this.draw(ctx, { ...renderProps, features }),
)
// 3. Return with explicit transferables
const serialized = { ...res, height, width: canvasWidth }
if (isImageBitmap(res.imageData)) {
return rpcResult(serialized, [res.imageData])
}
return serialized
}
}
Key differences:
- No
super.render()call - Canvas renderers handle everything themselves - Uses
rpcResult()- Explicitly specifies transferables for efficient transfer - No feature serialization - Features are rendered to canvas, not returned
- Uses
isImageBitmap()- Safe check that works in Node.js tests
The rpcResult pattern
The rpcResult() function from librpc-web-mod wraps return values with
explicit transferables:
import { rpcResult } from '@jbrowse/core/util/librpc'
// Without rpcResult - ImageBitmap would be copied (slow)
return { imageData: bitmap, width, height }
// With rpcResult - ImageBitmap is transferred (fast, zero-copy)
return rpcResult({ imageData: bitmap, width, height }, [bitmap])
When renderInWorker() sees an rpcResult, it passes through directly without
calling serializeResultsInWorker():
async renderInWorker(args) {
const results = await this.render(args2)
// rpcResult bypasses serialization
if (typeof results === 'object' && '__rpcResult' in results) {
return results
}
// Normal results go through serialization
return this.serializeResultsInWorker(results, args2)
}
The rpcResult wrapper is automatically unwrapped in
RpcMethodType.deserializeReturn(), so downstream code receives the actual
value regardless of whether a web worker was used.
Feature handling
Fetching features
FeatureRendererType.getFeatures() handles:
- Getting the data adapter from cache
- Expanding the region if needed (via
getExpandedRegion()) - Calling
adapter.getFeatures()orgetFeaturesInMultipleRegions() - Filtering features via
featurePassesFilters() - Returning a
Map<string, Feature>
Feature serialization
For React-based renderers, features are serialized:
// Worker side: Map<string, Feature> → SimpleFeatureSerialized[]
serializeResultsInWorker(result) {
return {
...serialized,
features: features instanceof Map
? iterMap(features.values(), f => f.toJSON(), features.size)
: undefined,
}
}
// Client side: SimpleFeatureSerialized[] → Map<string, SimpleFeature>
deserializeResultsInClient(result) {
const features = new Map(
result.features?.map(f => SimpleFeature.fromJSON(f)).map(f => [f.id(), f])
)
return { ...result, features }
}
Canvas renderers skip this by returning rpcResult() directly.
Layout management
BoxRendererType manages layouts for collision detection:
class BoxRendererType extends FeatureRendererType {
layoutSessions: Record<string, LayoutSession> = {}
createLayoutInWorker(args) {
const { layout } = this.getWorkerSession(args)
return layout.getSublayout(args.regions[0].refName)
}
async render(renderArgs) {
const features = await this.getFeatures(renderArgs)
const layout = this.createLayoutInWorker(renderArgs)
return { features, layout }
}
}
Layouts are cached by sessionId + trackInstanceId for incremental updates when
scrolling.
Creating a new renderer
Simple React-based renderer
import FeatureRendererType from '@jbrowse/core/pluggableElementTypes/renderers/FeatureRendererType'
class MyRenderer extends FeatureRendererType {
async render(renderArgs) {
const features = await this.getFeatures(renderArgs)
const result = await super.render({ ...renderArgs, features })
return { ...result, features }
}
}
Canvas-based renderer
import FeatureRendererType from '@jbrowse/core/pluggableElementTypes/renderers/FeatureRendererType'
import { renderToAbstractCanvas } from '@jbrowse/core/util'
import { isImageBitmap } from '@jbrowse/core/util/offscreenCanvasPonyfill'
import { rpcResult } from '@jbrowse/core/util/librpc'
class MyCanvasRenderer extends FeatureRendererType {
async render(renderProps) {
const features = await this.getFeatures(renderProps)
const { height, regions, bpPerPx } = renderProps
const region = regions[0]!
const width = (region.end - region.start) / bpPerPx
const res = await renderToAbstractCanvas(width, height, renderProps, ctx =>
this.draw(ctx, { ...renderProps, features }),
)
const serialized = { ...res, height, width }
if (isImageBitmap(res.imageData)) {
return rpcResult(serialized, [res.imageData])
}
return serialized
}
draw(ctx, props) {
// Custom canvas drawing logic
const { features } = props
for (const feature of features.values()) {
// Draw feature...
}
}
}
Renderer with layout (box-style)
import BoxRendererType from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
class MyBoxRenderer extends BoxRendererType {
// Uses default render() which fetches features and creates layout
// Override only if you need custom behavior
}
Utility functions
expandRegion
Expands a region by a number of base pairs:
import { expandRegion } from '@jbrowse/core/pluggableElementTypes/renderers/util'
getExpandedRegion(region, renderArgs) {
const bpExpansion = 100 // expand by 100bp each direction
return expandRegion(region, bpExpansion)
}
collectTransferables
Helper to collect all transferable objects from a render result:
import { collectTransferables } from '@jbrowse/core/util'
const serialized = { ...res, layout, height, width }
return rpcResult(serialized, collectTransferables(res))
This handles:
ImageBitmap(canvas image data)flatbushArrayBuffer (spatial index)subfeatureFlatbushArrayBuffer (secondary spatial index)
Verifying transfers work
After transfer, ArrayBuffers become "detached" (byteLength = 0) in the worker. You can verify transfers are working by checking:
import { isDetachedBuffer } from '@jbrowse/core/util/transferables'
// In worker, after rpcResult returns:
console.log('Buffer detached:', isDetachedBuffer(flatbush.data))
// Should be true if transfer worked
In Chrome DevTools, the Performance panel shows postMessage events and whether
transferables were used (look for "Transferable" in the details).
SVG export
During SVG export, there's no ImageBitmap available. Canvas renderers handle
this by always using rpcResult() with collectTransferables():
// collectTransferables returns [] when no ImageBitmap
return rpcResult(serialized, collectTransferables(res))
The serialization methods handle both cases:
- Live layout objects get serialized via
serializeRegion() - Already-serialized layouts pass through unchanged