Back to List

High-performance sequence viewer: Render 1 million base pairs in real-time within your browser using WebAssembly.

We will implement performance-critical logic in Rust, compile it to WebAssembly (WASM), and bridge it with JavaScript to create a high-performance DNA sequence viewer for the browser. The goal is to achieve performance comparable to IGV.js.

Advanced
|
100min
|
Verified (2026-07)
Ranked viewer list.WebAssemblyIntravenous glucosegenome browserCanvas rendering.High-performance web.Rust for WebAssembly
Progress0/19 (0%)

High-Performance Serial Viewer โ€” Render 1 Million Base Pairs in Real Time in the Browser Using WebAssembly

Upon Completion of This Topic

You will be able to create your own viewer that smoothly renders large DNA sequences in the browser by combining the WebAssembly, DOM, and Big-O complexity concepts you learned in the textbook. You will gain an understanding of the principles behind practical tools like IGV.js through code.

This article is an educational, general example. Real-world sequence viewers are much more sophisticated, but this covers the core principles of performance optimization.


"My Browser Froze!" โ€“ The Pitfalls of Naive Rendering

Let's say you want to display a 1 million base pair DNA sequence in a browser.

First attempt:

javascript
function renderSequence(sequence) {
  const container = document.getElementById("viewer");
  for (let i = 0; i < sequence.length; i++) {
    const span = document.createElement("span");
    span.textContent = sequence[i];
    span.className = `base-${sequence[i]}`;
    container.appendChild(span);
  }
}

renderSequence(oneMillionBp);

Running this code will cause the browser to freeze for over 5 seconds before it finally renders. And the page scrolling will be severely laggy.

The problems:

Problem 1: 1 million DOM nodes. Each span has a memory overhead of at least a few KB. In total, that's several GB.

Problem 2: DOM insertion is close to O(nยฒ). Each appendChild triggers a layout recalculation for all previous nodes.

Problem 3: String iteration is slow in JavaScript. Looping through 1 million characters is much slower than native code.

The real approach focuses on three axes:

  1. Render with Canvas instead of DOM (no 1 million nodes)
  2. Virtualization โ€“ only render the sequence within the viewport (O(viewport) complexity)
  3. WebAssembly โ€“ run performance-critical logic at native speed

From Black Box to Components

Component 1: Sequence Viewer - Big-O Perspective

Analyze the computational cost required for each frame of the sequence viewer.

Naive Approach: Scan the entire sequence in each frame. O(n).

Virtualization: Render only the portion of the sequence that is within the viewport. O(viewport).

Indexing: Enable O(1) lookup for specific positions.

For a 1 million bp sequence, with a viewport of, for example, 200 bp:

  • O(n) = 1 million operations
  • O(viewport) = 200 operations
  • 5,000x faster

This difference is key to achieving 60fps real-time rendering.

Component 2: Canvas-Based Rendering

Use a single canvas instead of the DOM.

javascript
function initCanvas(container) {
  const canvas = document.createElement("canvas");
  canvas.width = container.clientWidth;
  canvas.height = 100;
  container.appendChild(canvas);
  return canvas.getContext("2d");
}


function renderViewport(ctx, sequence, startBp, endBp, bpWidth) {
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  const colors = { A: "#66c2a5", C: "#fc8d62", G: "#8da0cb", T: "#e78ac3" };
  
  for (let i = startBp; i < endBp && i < sequence.length; i++) {
    const x = (i - startBp) * bpWidth;
    const base = sequence[i];
    ctx.fillStyle = colors[base] || "#ccc";
    ctx.fillRect(x, 20, bpWidth, 40);
    
    if (bpWidth >= 8) {
      ctx.fillStyle = "black";
      ctx.font = "12px monospace";
      ctx.fillText(base, x + 2, 45);
    }
  }
}

The canvas holds the rendered portion as a visual representation. During a scroll event, simply update startBp and endBp and redraw.

Component 3: Offload Performance-Critical Logic to WebAssembly

Canvas rendering itself is sufficient in JavaScript. However, for performance-critical tasks such as GC content calculation, motif searching, and sorting, WebAssembly offers advantages.

Example of GC content calculation in Rust:

rust
// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn gc_content_window(sequence: &str, window_size: usize) -> Vec<f32> {
    let bytes = sequence.as_bytes();
    let n = bytes.len();
    if n < window_size {
        return vec![];
    }
    
    let mut result = Vec::with_capacity(n - window_size + 1);
    let mut gc_count: usize = 0;
    
    for i in 0..window_size {
        if bytes[i] == b'G' || bytes[i] == b'C' {
            gc_count += 1;
        }
    }
    result.push(gc_count as f32 / window_size as f32);
    
    for i in window_size..n {
        if bytes[i] == b'G' || bytes[i] == b'C' {
            gc_count += 1;
        }
        if bytes[i - window_size] == b'G' || bytes[i - window_size] == b'C' {
            gc_count -= 1;
        }
        result.push(gc_count as f32 / window_size as f32);
    }
    
    result
}

Build:

bash
wasm-pack build --target web

This will generate the .wasm file and the JS bridge in the pkg/ folder.

Usage in the browser:

javascript
import init, { gc_content_window } from "./pkg/sequence_viewer.js";

async function main() {
  await init();
  
  const sequence = "ATGCGATCGATCG...".repeat(100000);
  
  console.time("WASM GC");
  const gcArray = gc_content_window(sequence, 100);
  console.timeEnd("WASM GC");
  
  console.log(`Computed GC for ${gcArray.length} windows`);
}

For a 1 million bp sequence, calculating sliding window GC:

  • Pure JavaScript: ~500ms
  • WASM: ~15ms
  • ~30x faster

Component 4: Scroll Events and Render Loop

Smooth scrolling = 60fps = within 16ms per frame.

javascript
class SequenceViewer {
  constructor(container, sequence) {
    this.ctx = initCanvas(container);
    this.sequence = sequence;
    this.startBp = 0;
    this.bpWidth = 4;
    this.viewportBp = Math.floor(this.ctx.canvas.width / this.bpWidth);
    
    this.attachEvents(container);
    this.render();
  }
  
  attachEvents(container) {
    container.addEventListener("wheel", (e) => {
      e.preventDefault();
      const delta = Math.sign(e.deltaY) * 10;
      this.startBp = Math.max(0, Math.min(this.sequence.length - this.viewportBp, this.startBp + delta));
      requestAnimationFrame(() => this.render());
    });
  }
  
  render() {
    const endBp = Math.min(this.sequence.length, this.startBp + this.viewportBp);
    renderViewport(this.ctx, this.sequence, this.startBp, endBp, this.bpWidth);
  }
}

Key: Align with the browser's render cycle using requestAnimationFrame. This automatically achieves 60fps.


Fading โ€” Three Blanks for You to Fill

Blank 1: Multi-Track Renderer

Overlays multiple tracks, such as read alignments and gene annotations, on top of a sequence.

javascript
class MultiTrackViewer extends SequenceViewer {
  constructor(container, sequence, tracks) {
    super(container, sequence);
    this.tracks = tracks;
    this.ctx.canvas.height = 100 + tracks.length * 30;
  }
  
  render() {
    super.render();
    
    // TODO: Draw each track below the sequence.
    // track = { name: string, features: [{start, end, color, label}] }
    // Only draw features within the viewport range.
    for (let i = 0; i < this.tracks.length; i++) {
      // TODO: Convert the (start, end) of each feature to canvas coordinates and draw a rectangle.
    }
  }
}

Hint: const xStart = (feature.start - this.startBp) * this.bpWidth; const xEnd = (feature.end - this.startBp) * this.bpWidth; if (xEnd < 0 || xStart > canvas.width) continue;.

Blank 2: WASM Motif Search

Implement the previously discussed trie in Rust and expose it as WASM.

rust
#[wasm_bindgen]
pub fn find_motif(sequence: &str, motif: &str) -> Vec<usize> {
    // TODO: Return all positions where the motif appears in the sequence.
    // Hint: sequence.match_indices(motif).map(|(i,_)| i).collect()
    vec![]
}

Usage in the browser:

javascript
const positions = find_motif(sequence, "GAATTC");  // EcoRI restriction site
positions.forEach(pos => drawMarker(pos));

Blank 3: Performance Benchmark

Compare the performance of a pure JavaScript implementation with a WASM implementation.

javascript
async function benchmark() {
  const sequence = "ATCGATCG".repeat(125_000);  // 1 million bp
  
  // TODO 1: Implement a pure JavaScript GC calculation function (as a reference).
  function gcContentJs(seq, window) {
    // Your implementation
  }
  
  // TODO 2: Measure the time for both JS and WASM.
  // console.time / console.timeEnd
  
  // TODO 3: Verify that the results are the same (e.g., samples[0], samples[500000], samples[999900]).
  
  // TODO 4: Display the results.
}

Reflections โ€” Differences from a Production-Ready Genome Viewer

IGV.js: JavaScript port of the Integrative Genomics Viewer. Supports various standard formats such as BAM, VCF, and BED. Operates with only the client-side code, without a backend server.

Ensembl ยท UCSC Genome Browser: Representative web-based genome browsers. They have sophisticated backend infrastructure and cache various track data.

JBrowse 2: A modern architecture genome browser. Combines WebWorker, WASM, and React. Supports very large datasets.

Deep learning visualization: Recent tools like AlphaFold require 3D structure visualization. WebGL/WebGPU is needed.

Streaming processing: In production, the entire sequence is not loaded into the browser. Instead, a range request pattern is used to request only the necessary parts from the server. Web Sockets/HTTP2 push.

Extension Project

1. FASTA Loader: Load a FASTA file into the viewer when the user drags and drops it.

2. GFF/BED Parser: Parse gene annotation files and display them as tracks.

3. Search UI: Implement a text input field for searching motifs. Highlight the results on the minimap.

4. Export: Save the current viewport as a PNG/SVG image.

Component Implementation Guide

  • [F] WebAssembly: Rust โ†’ wasm-pack โ†’ Browser loading. JS-WASM bridge.
  • [F] DOM: Minimize DOM manipulation. Replace 1 million elements with a single canvas.
  • [F] Big-O: O(n) โ†’ O(viewport) virtualization. Concept of performance budget.
  • [W] HTML/JS Fundamentals: Event handling, requestAnimationFrame (complete script provided).

[F] = You implement it yourself / [W] = Provided as complete code.

๐Ÿ’ฌ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...