Volver a la lista

Visor de secuencias de alto rendimiento: renderizado en tiempo real de 1 millón de pares de bases en el navegador con WebAssembly.

Implemente la lógica fundamental para optimizar el rendimiento en Rust, compile el código a WebAssembly (WASM) y conéctelo a JavaScript para crear un visor de secuencias de ADN de gran tamaño que se ejecute en el navegador. Este es el principio que permite alcanzar un rendimiento similar al de IGV.js.

Avanzado
|
100min
|
Verificado (2026-07)
Visor de series.WebAssemblyIGVgenome browserrenderizado en el lienzoSitio web de alto rendimiento.Rust wasm
Progreso0/19 (0%)

Visor de secuencias de alto rendimiento: renderizado en tiempo real de 1 millón de pares de bases en el navegador con WebAssembly

Al finalizar este tema

Podrás integrar los conceptos de WebAssembly, DOM y complejidad Big-O aprendidos en los libros de texto para crear directamente un visor que renderice de forma fluida secuencias de ADN de gran volumen en el navegador. Comprenderás, a través del código, los principios que subyacen a herramientas prácticas como IGV.js.

Este artículo es un ejemplo educativo general. Los visores de secuencias reales son mucho más sofisticados, pero aquí se abordan los principios de optimización del rendimiento que están en su núcleo.


"El navegador se ha congelado": la trampa del renderizado ingenuo

Imagina que intentas mostrar una secuencia de ADN de 1 millón de pares de bases en el navegador.

Primer intento:

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

Al ejecutar este código, el navegador se congela durante más de 5 segundos antes de renderizar. Además, el desplazamiento de la página es muy lento.

Problema:

Problema 1: 1 millón de nodos DOM. Cada elemento span tiene una sobrecarga de memoria de al menos varios KB. ¿Cuántos GB en total?

Problema 2: La inserción en el DOM se acerca a O(n²). appendChild provoca el recálculo del diseño de los nodos anteriores.

Problema 3: La iteración sobre cadenas es lenta en JavaScript. Iterar 1 millón de caracteres es mucho más lento que las operaciones nativas.

El enfoque real se basa en tres ejes:

  1. Renderizar con Canvas en lugar de DOM (sin 1 millón de nodos)
  2. Virtualización: renderizar solo la secuencia dentro del área visible (complejidad O(área visible))
  3. WebAssembly: ejecutar la lógica crítica de rendimiento a velocidad nativa

De caja negra a componentes

Componente 1: Visor de secuencias desde la perspectiva de la notación Big O

Analizamos la cantidad de cálculos necesarios para cada fotograma del visor de secuencias.

Enfoque ingenuo: escanear toda la secuencia en cada fotograma. O(n).

Virtualización: renderizar solo la parte que cabe en el área visible. O(área visible).

Índice: consultar una posición específica en O(1).

Si el área visible es de, por ejemplo, 200 pb dentro de 1 millón de pb:

  • O(n) = 1 millón de operaciones
  • O(área visible) = 200 operaciones
  • 5.000 veces más rápido

Esta diferencia es la clave para lograr un renderizado en tiempo real a 60 fps.

Componente 2: Renderizado basado en Canvas

En lugar del DOM, se utiliza un único 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);
    }
  }
}

El canvas contiene una representación visual de la parte renderizada. En el evento de desplazamiento, basta con actualizar startBp y endBp y volver a dibujar.

Componente 3: Optimización del rendimiento con WebAssembly

El renderizado del canvas en sí puede ser suficiente con JavaScript. Sin embargo, si existen tareas que requieren un alto rendimiento, como el cálculo del contenido de GC, la búsqueda de motivos o el ordenamiento, WebAssembly ofrece ventajas.

Ejemplo de cálculo del contenido de GC en 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
}

Compilación:

bash
wasm-pack build --target web

En la carpeta pkg/ se encontrarán .wasm y el puente JS.

Para usar en el navegador:

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

Cálculo de la proporción de GC mediante una ventana deslizante de 1 millón de pares de bases:

  • JavaScript puro: ~500 ms
  • WASM: ~15 ms
  • ~30 veces más rápido

Componente 4: Eventos de desplazamiento y ciclo de renderizado

Desplazamiento suave = 60 fps = menos de 16 ms por fotograma.

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

Clave: Sincronizar con el ciclo de renderizado del navegador mediante requestAnimationFrame. 60 fps automático.


Desvanecimiento: los tres espacios en blanco que deben completarse

Espacio en blanco 1: Renderizado de múltiples capas

Superponga varias capas, como la alineación de lecturas y la anotación de genes, sobre la secuencia.

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: dibujar cada pista debajo de la secuencia
    // pista = { name: string, features: [{start, end, color, label}] }
    // dibujar solo las características dentro del rango del viewport
    for (let i = 0; i < this.tracks.length; i++) {
      // TODO: convertir (start, end) de cada característica a coordenadas del canvas y dibujar un rectángulo
    }
  }
}

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

Espacio en blanco 2: Búsqueda de motivos WASM

Implementar el trie que se analizó anteriormente en Rust y exponerlo como WASM.

rust
#[wasm_bindgen]
pub fn find_motif(sequence: &str, motif: &str) -> Vec<usize> {
    // TODO: devolver todas las posiciones donde aparece el motivo en la secuencia
    // Pista: sequence.match_indices(motif).map(|(i,_)| i).collect()
    vec![]
}

Usar en el navegador:

javascript
const positions = find_motif(sequence, "GAATTC");  // sitio de corte de EcoRI
positions.forEach(pos => drawMarker(pos));

Espacio vacío 3: Pruebas comparativas de rendimiento

Comparación del rendimiento entre la implementación en JavaScript y la implementación en WASM.

javascript
async function benchmark() {
  const sequence = "ATCGATCG".repeat(125_000);  // 1 millón de pares de bases

  // TODO 1: implementar la función de cálculo GC en JS puro (referencia)
  function gcContentJs(seq, window) {
    // Implementado por ti
  }

  // TODO 2: medir el tiempo para JS y WASM respectivamente
  // console.time / console.timeEnd

  // TODO 3: verificar que los resultados sean iguales (por ejemplo, samples[0], samples[500000], samples[999900])

  // TODO 4: mostrar resultados
}

Reflexión: Diferencias con un visor de secuencias en producción

IGV.js: Versión en JavaScript del Integrative Genomics Viewer. Es compatible con varios formatos estándar, como BAM, VCF y BED. Funciona únicamente en el cliente, sin necesidad de un servidor backend.

Ensembl · UCSC Genome Browser: Navegadores genómicos basados en web, ampliamente reconocidos. Su infraestructura backend es sofisticada y los datos de múltiples pistas se almacenan en caché.

JBrowse 2: Navegador genómico con una arquitectura moderna. Combina Web Worker, WASM y React. Admite conjuntos de datos muy grandes.

Visualización con aprendizaje profundo: Herramientas recientes, como AlphaFold, requieren la visualización de estructuras 3D. Esto implica el uso de WebGL/WebGPU.

Procesamiento en streaming: En entornos de producción, no se carga toda la secuencia en el navegador. Se utiliza un patrón de solicitud por rango, en el que el servidor solicita solo las partes necesarias, junto con WebSockets o HTTP2 push.


Proyectos de expansión

1. Cargador FASTA: Permite al usuario arrastrar y soltar un archivo FASTA para cargarlo en el visor.

2. Analizador GFF/BED: Analiza archivos de anotación génica y los muestra como pistas.

3. Interfaz de búsqueda: Permite buscar motivos mediante la introducción de texto. Los resultados se resaltan en el minimapa.

4. Exportación: Guarda la vista actual del visor como PNG/SVG.


Mapa de componentes de este capítulo

  • [F] WebAssembly: Rust → wasm-pack → carga en el navegador. Puente JS-WASM.
  • [F] DOM: Manipulación mínima del DOM. Un único lienzo (Canvas) reemplaza a 100.000 elementos.
  • [F] Big-O: De O(n) a O(ventana de visualización) mediante virtualización. Concepto de presupuesto de rendimiento.
  • [W] Fundamentos de HTML/JS: Manejo de eventos, requestAnimationFrame (se proporciona el script completo).

[F] = Implementado por ustedes / [W] = Código completo proporcionado.

💬 Preguntas y comentarios

0 comentarios

Puedes publicar sin iniciar sesión. Los comentarios de invitados no pueden editarse ni eliminarse después.

0/2000

Cargando...