一覧へ

高性能なシーケンスビューワー。WebAssemblyを使用し、100万塩基対のデータをブラウザ上でリアルタイムに表示。

Rustを使って、パフォーマンスが重要な処理を行うロジックを記述し、それをWASMにコンパイルします。そして、JavaScriptと連携させて、ブラウザ上で大規模なDNA配列を効率的に表示できるビューアを作成します。IGV.jsと同等のパフォーマンスを目指します。

上級
|
100
|
検証済み (2026-07)
ランキング表示機能WebアセンブリIGVゲノムブラウザキャンバスへの描画高性能なウェブサイトRustのWebAssembly
進捗0/19 (0%)

高性能シーケンスビューワー — WebAssembly を使用して、100 万 bp をブラウザでリアルタイムにレンダリング

このトピックを終えると

教科書で学んだ WebAssemblyDOMBig-Oの計算量を組み合わせて、大規模なDNA配列をブラウザでスムーズにレンダリングするビューワーを自分で作成できます。IGV.jsのような実用的なツールの背後にある原理をコードで理解します。

この記事は教育目的の一般的な例です。実用的な配列ビューワーはさらに高度ですが、その中核となるパフォーマンス最適化の原理をここで扱います。


「ブラウザがフリーズした」-素朴なレンダリングの落とし穴

100万bpのDNA配列をブラウザに表示するとしましょう。

最初の試み:

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);

このコードを実行すると、ブラウザが5秒以上フリーズした後、ようやくレンダリングされます。そして、ページスクロールもひどくカクカクします。

問題点:

問題1: DOMノードが100万個。各spanのメモリオーバーヘッドは最小でも数KB。全体で数GB。

問題2: DOMの挿入はO(n²)に近い。appendChildの操作が、それまでのノードのレイアウトを再計算する。

問題3: 文字列の反復処理がJSで遅い。100万文字の反復処理は、ネイティブコードよりもはるかに遅い。

真の解決策は、次の3つの軸にあります。

  1. DOMの代わりにCanvasでレンダリング(100万のノードは不要)
  2. 仮想化 – ビューポート内の配列のみをレンダリング(O(ビューポート)の複雑さ)
  3. WebAssembly – パフォーマンスが重要なロジックをネイティブ速度で実行

ブラックボックスからコンポーネントへ

コンポーネント 1: Big-O の観点からのシーケンスビューワー

シーケンスビューワーの各フレームで必要な計算量を分析します。

単純なアプローチ: 各フレームでシーケンス全体をスキャン。O(n)。

仮想化: ビューポートに入る部分だけをレンダリング。O(ビューポート)。

インデックス: 特定の位置へのアクセスを O(1) に。

100万 bp で、ビューポートが例えば 200 bp の場合:

  • O(n) = 100万回の演算
  • O(ビューポート) = 200回の演算
  • 5,000倍高速化

この差が、60fps のリアルタイムレンダリングの鍵です。

コンポーネント 2: Canvas ベースのレンダリング

DOM の代わりに、単一の canvas を使用します。

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);
    }
  }
}

単一の canvas がレンダリングされた部分を画像として保持します。スクロールイベントで startBpendBp のみを更新し、再度描画するだけです。

コンポーネント 3: WebAssembly でパフォーマンスクリティカルなロジックをオフロード

canvas レンダリング自体は JavaScript で十分です。ただし、GC (グアニン-シトシン) 含有量の計算モチーフ検索ソート などのパフォーマンスクリティカルなタスクがある場合は、WASM を使用する方が有利です。

Rust で GC 含有量の計算の例:

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
}

ビルド:

bash
wasm-pack build --target web

pkg/ フォルダに .wasm と JavaScript ブリッジが出力されます。

ブラウザでの使用:

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`);
}

100万 bp での、スライディングウィンドウによる GC 含有量の計算:

  • 純粋な JavaScript: ~500ms
  • WASM: ~15ms
  • ~30倍高速化

コンポーネント 4: スクロールイベントとレンダーループ

スムーズなスクロール = 60fps = 1フレームあたり 16ms 以内。

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);
  }
}

重要な点: requestAnimationFrame を使用して、ブラウザのレンダリングサイクルに同期します。これにより、60fps が自動的に実現されます。

フェーディング — 埋めるべき3つの空白

空白1:マルチトラックレンダラー

配列の上に、リードアライメントや遺伝子アノテーションなど、複数のトラックを重ねて描画します。

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: 各トラックを配列の下に描画する
    // トラック = { name: string, features: [{start, end, color, label}] }
    // ビューポートの範囲内にあるフィーチャーのみを描画する
    for (let i = 0; i < this.tracks.length; i++) {
      // TODO: 各フィーチャーの (start, end) をキャンバス座標に変換し、長方形を描画する
    }
  }
}

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

空白2:WASMモチーフ検索

先に扱ったトライをRustで実装し、WASMとして公開します。

rust
#[wasm_bindgen]
pub fn find_motif(sequence: &str, motif: &str) -> Vec<usize> {
    // TODO: シーケンスからモチーフが出現するすべての位置を返す
    // ヒント: sequence.match_indices(motif).map(|(i,_)| i).collect()
    vec![]
}

ブラウザでの使用:

javascript
const positions = find_motif(sequence, "GAATTC");  // EcoRI切断部位
positions.forEach(pos => drawMarker(pos));

空白3:パフォーマンスベンチマーク

純粋なJS実装とWASM実装のパフォーマンス比較。

javascript
async function benchmark() {
  const sequence = "ATCGATCG".repeat(125_000);  // 100万bp
  
  // TODO 1: 純粋なJS GC計算関数を実装する(参考)
  function gcContentJs(seq, window) {
    // あなたの実装
  }
  
  // TODO 2: JSとWASMそれぞれで時間を計測する
  // console.time / console.timeEnd
  
  // TODO 3: 結果が同じであることを検証する(samples[0], samples[500000], samples[999900]など)
  
  // TODO 4: 結果を表示する
}

考察:実戦的な配列ビューアとの違い

IGV.js: Integrative Genomics ViewerのJavaScript版。BAM、VCF、BEDなど、複数の標準フォーマットをサポート。バックエンドサーバーなしで、クライアントだけで動作する。

Ensembl・UCSC Genome Browser: ウェブベースのゲノムブラウザの代表。バックエンドのインフラが洗練されており、複数のトラックデータがキャッシュされている。

JBrowse 2: 現代的なアーキテクチャを持つゲノムブラウザ。WebWorker、WASM、Reactの組み合わせ。非常に大きなデータセットをサポートする。

深層学習の可視化: 近年、AlphaFoldのようなツールが3D構造の可視化を必要とする。WebGL/WebGPUが必要。

ストリーミング処理: 実践的な場面では、配列全体をブラウザにロードしない。必要な部分だけをサーバーからリクエストするレンジリクエストパターン。Web Sockets/HTTP2 push。


拡張プロジェクト

1. FASTAローダー: ユーザーがFASTAファイルをドラッグ&ドロップすると、ビューアーにロードする。

2. GFF/BEDパーサー: 遺伝子アノテーションファイルを解析し、トラックとして表示する。

3. 検索UI: テキスト入力でモチーフを検索する。結果をミニマップにハイライト表示する。

4. エクスポート: 現在のビューポートをPNG/SVG形式で保存する。


この機能の構成要素

  • [F] WebAssembly: Rust → wasm-pack → ブラウザにロード。JS-WASMブリッジ。
  • [F] DOM: 最小限のDOM操作。Canvasを1つ使用して100万個の要素を代替。
  • [F] Big-O: O(n) → O(ビューポート) 仮想化。パフォーマンス予算の概念。
  • [W] HTML/JSの基礎: イベントハンドリング、requestAnimationFrame(完成したスクリプトを提供)。

[F] = 自分で実装する / [W] = 完成したコードとして提供。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...