Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Sparrowhawk is a web platform for doing basic bioinformatic analyses on bacterial genomes locally. It offers genome assembly, sequence mapping and alignment, taxonomic identification, gene calling, and host depletion.

You can see the different sections of this guide in the left, where each of the tabs/methodologies usage is detailed.

Why use Sparrowhawk?

All analyses are run in your own system, with nothing being uploaded anywhere, nor a job running in a random foreign computing cluster: this makes Sparrowhawk ideal for resource-constrained setups (e.g. bad Internet connection), or for sensitive data studies. The methods are provided as web pages, being thus intuitive to be used, as well as available when you are using a tablet (or even a smartphone!).

Code repositories

All code used is open source and most of it has been developed inside the bacterial population genomics group (bacpop) at EMBL’s European Bioinformatics Institute (EMBL-EBI). The main code repositories are the following:

Developers who want to adapt their own tools to run in the browser should consult the Developer guide.

Assembly

Genome assembly consists on recovering a genome (the DNA sequence) from organisms that have been sequenced. Sparrowhawk offers this service through a novel assembler, written in Rust, that uses as main structure a node-based de Bruijn graph.

General considerations

  • The assembler has been designed with bacterial genomes (\( O(10^0-10^1)\,\text{Mbp} \)) on mind, so large read files might not be work properly with it.
  • The aim for its development was for it to be efficient and able to be loaded into a website (using WebAssembly), so it is not the most thorough and detailed genomic assembler. Its performance is overall similar/superior to Velvet, and similar to that of Minia (with single k-value).
  • Given a current technical limitation1 in the amount of memory that can be used (4 GB), high coverage reads or those coming from large genome species might reach that limit.
  • If you want to run this assembler locally in the command line, you can find more information in its GitHub repository.

Parameters

The values in brackets are the default ones:

  • k [31]: controls the size of subsequences used in the algorithm. It is an odd integer that should be between ~21 up until 255.
  • Minimum Illumina read quality [20]: the quality of the input fastq files, it is an integer from zero to 33.
  • Minimum counts for k-mer filtering [5]: this value controls the cutoff for filtering low-count k-mers, that are usually associated to sequencing errors. Low values will imply larger memory usage. Its value is correlated with k. It is recommended to check in the k-mer spectrum histogram how much of it is filtered. It should not take the main bell part of the distribution, only the peak that should be near one or two.
  • Automatically set the minimum counts for k-mer filtering [false]: the assembler has an automatic setting for fixing the previous parameter, that fits the k-mer spectrum distribution and extracts the ideal value. If using, it is recommended to check in the k-mer spectrum plot the chosen value.
  • Chunk processing size [150000]: reads are by default processed in chunks, reducing the memory usage. Their size can be modified here, or disabled completely (by setting it to zero).
  • Use Bloom filter for preprocessing [false]: if facing issues with memory usage, try using a Bloom filter for the preprocessing step. This significantly reduces the memory used, while allowing for some degree of overcounting some k-mers.
  • Do not remove dead-ends [false]: this removes the dead-end removal step in the error correction algorithm of the assembler.
  • Do not collapse bubbles [false]: this removes the simple bubble removal step in the error correction algorithm of the assembler.

Example

The following data can be used to try out the assembler:

  • Species: Mycobacterium tuberculosis
  • Reads (ENA accession ERR8158023): forward and reverse

  1. The intermediate libraries for compiling Rust code into WebAssembly have not implemented the memory64 proposal, that allows to have 64 bit memory addresses (thus removing the 4GB memory limit). Once this is done, compilation to wasm64-unknown-unknown should be easily possible with them and thus we would remove this annoying limitation.

Mapping

Mapping is often used as an alternative way of reconstructing a genome from sequencing raw data, instead of trying to re-assemble it from scratch. Sparrowhawk offers this service through ska.rust (paper): a toolkit for prokaryotic (and other haploid and small genome beings) sequence analysis with split kmers.

General considerations

After processing, the mapped sequences are displayed in an interactive MSEABOARD sequence viewer directly in the browser.

Parameters

The values in brackets are the default ones:

  • k [31]: controls the size of subsequences used in the algorithm. It is an odd integer that should be between ~17 up until 63.
  • Proportion of reads [1.0]: a real number between 0 and 1 that allows you to choose, when you upload reads, what fraction of those you want to sample (if you want to do so).
  • Min Illumina read quality [20]: integer 0–33; filters nucleotides below this quality score.
  • Quality filter type [All bases]: dropdown; “No filter” / “Middle base” (only the central split-kmer base) / “All bases” (any base in the k-mer).
  • Use canonical k-mers [false]: accounts for both strand orientations; recommended when using raw reads. Can only be set before uploading any file.
  • Mask ambiguous bases [false]: replaces ambiguous bases in the output with N. Can only be set before uploading any file.
  • Mask repeats [false]: masks repeated regions in the output with N. Can only be set before uploading any file.

Example

The following data can be used to try out the mapper:

Alignment

Alignment is a way of showing the differences between two or more DNA sequences, often used in evolution/phylogenetic studies. Sparrowhawk offers this service through ska.rust (paper): a toolkit for prokaryotic (and other haploid and small genome beings) sequence analysis with split kmers.

General considerations

  • Take into account that ska.rust (the underlying methodology) is designed to work with closely related genomes (less than species level). If you use very different sequences, the results might not be accurate.
  • You will need three or more sequences to perform the alignment.
  • After processing, the alignment is displayed in an interactive multiple sequence alignment (MSA) viewer in the browser, with the phylogenetic tree shown alongside it.
  • The phylogenetic tree is computed with the speedytree crate and rendered with Taxonium (paper).

Parameters

The values in brackets are the default ones:

  • k [31]: controls the size of subsequences used in the algorithm. It is an odd integer that should be between ~17 up until 63.
  • Proportion of reads [1.0]: a real number between 0 and 1 that allows you to choose, when you upload reads, what fraction of those you want to sample (if you want to do so).
  • Min Illumina read quality [20]: integer 0–33; filters nucleotides below this quality score.
  • Quality filter type [All bases]: dropdown; “No filter” / “Middle base” (only the central split-kmer base) / “All bases” (any base in the k-mer).
  • Use canonical k-mers [false]: accounts for both strand orientations; recommended when using raw reads. Can only be set before uploading any file.

Example

The following data can be used to try out the aligner:

  • Species: Escherichia coli (three or more assemblies required)
  • Dataset: ENA study PRJEB23541 — Tanzanian hospital isolates. Click any “Accession” value in the study table, then click “SET FASTA” in the right panel to load each assembly.

Taxonomic identification

This tab will try to assess what bacterial species correspond to any reads or genomic sequences that you “upload” (load into memory really) in it. Sparrowhawk offers this service through sketchlib.rust: a further developed Rust port of pp-sketchlib, that allows for fast comparisons of sequences. The data for the reference database comes from the AllTheBacteria dataset (preprint), that comprises close to 2.5 million bacterial genomes uniformly assembled. These have been clustered using gemsparcl, to obtain high-quality groups of sequences whose species we know.

General considerations

  • The underlying code uses an inverted index (already implemented inside sketchlib.rust), built with sketches (akin to “fingerprints”)1 of representatives of high-quality clusters extracted from the AllTheBacteria dataset. When one sample is uploaded to this web page (either as a FASTA or FASTQ file), it is sketched as well, and its sketch is compared with the representative ones using sketchlib.rust’s inverted index functionality. The sketches with highest Jaccard index are returned.
  • Multiple files are processed simultaneously, one per worker, using parallel Web Workers in the browser.

Parameters

The values in brackets are the default ones:

  • Min Illumina read quality [20]: integer 0–33; filters nucleotides below this quality score when processing FASTQ reads.
  • Min counts for k-mer filtering [5]: integer 1–30; only k-mers appearing more than this threshold are used. Analogous to the assembly parameter of the same name.
  • Proportion of reads [1.0]: a real number between 0 and 1 that allows you to choose what fraction of reads to sample.
  • Workers [4]: integer 1–8; number of parallel web workers processing files simultaneously. Higher values speed up batch identification but use more memory.

Example

The following data can be used to try out taxonomic identification:

  • Species: Klebsiella pneumoniae
  • Assembly: GCA_004138665.1 (FASTA, gzipped) from ENA

  1. See the sketchlib.rust code repository for more documentation on how sketches are obtained from a genome.

Gene calling

Gene calling predicts protein-coding genes (open reading frames) in bacterial genome sequences. Sparrowhawk offers this through Orphos, a Rust port of Prodigal (paper), a widely-used prokaryotic gene predictor.

General considerations

  • Accepts FASTA files (plain or gzipped). Multiple files can be processed in parallel across workers.
  • Two operating modes:
    • Single-genome mode (default, metag=false): all contigs in a file are first concatenated (separated by stop-codon bridges) to train the gene model on the whole genome, then each contig is analysed independently with that trained model. Best for complete or near-complete assemblies.
    • Metagenomic mode (metag=true), also called anonymous: each contig is analysed independently with a pre-trained general model. Recommended for metagenome-assembled genomes (MAGs) or any file with many short, unrelated sequences.
  • Output files (downloadable after analysis):
    • GFF file with called genes.

Parameters

The values in brackets are the default ones:

  • Workers [4]: integer 1–8; number of parallel web workers. Multiple input files are distributed across workers.
  • Translation table [Default/Auto]: NCBI translation table (1–25, or 0 for auto-detection). See the official NCBI list for more information.
  • Use metagenomic mode [false]: see General considerations above.
  • Ignore truncated genes [false]: suppress reporting of genes that run off the edge of a contig (open ends).
  • Break calling on N subsequences [false]: do not bridge over runs of unknown (N) bases when predicting genes.
  • Ignore Shine-Dalgarno sequences [false]: force the algorithm to not use Shine-Dalgarno ribosome-binding site signals in gene calling.

Example

The following data can be used to try out gene calling:

  • Species: Klebsiella pneumoniae
  • Assembly: GCA_004138665.1 (FASTA, gzipped) from ENA

Host depletion

Host depletion filters sequencing reads to remove (or isolate) reads that match a host genome index, retaining the reads of interest for downstream analysis. Sparrowhawk uses Deacon (preprint) for this purpose.

General considerations

  • Requires uploading a pre-built Deacon index (.dci file) for the host genome of interest. The Deacon repository contains pre-built indexes for common hosts (human, etc.).
  • Accepts FASTQ reads (plain or gzipped); output is written in the same format (optionally gzip-compressed).
  • The two thresholds (absolute and relative) are applied together: a read must satisfy both to be classified as host-derived.

Parameters

The values in brackets are the default ones:

  • Deplete mode [enabled]: when enabled, reads matching the host index are removed from the output. When disabled, matching reads are kept instead (useful for isolating host reads).
  • Absolute threshold [1]: integer 1–50; minimum number of k-mers that must match the host index for a read to be classified as host-derived.
  • Relative threshold [0.05]: real 0–1; minimum proportion of a read’s k-mers that must match the host index.

Example

The following data can be used to try out host depletion:

Guide to compile methods or tools to WebAssembly

This guide is aimed at bioinformaticians who are comfortable writing Rust and want to bring their tools to the web via WebAssembly (wasm). It covers the essential concepts, toolchain, workflow, and known limitations, using the same approach as this repository: Rust compiled with wasm-pack, and a plain-JavaScript front end that calls into the resulting module.


Table of Contents

  1. What is WebAssembly?
  2. Available targets
  3. Toolchain
  4. Workflow
  5. Designing the Rust interface
  6. Building and bundling
  7. Web integration
  8. Limitations
  9. A note on WebGPU
  10. Minimum working example (MWE)

What is WebAssembly?

WebAssembly (wasm) is a binary instruction format designed as a portable compilation target for high-level languages, though can be loosenly defined as a somewhat-universal compilation target. It can run inside the browser (and increasingly outside of it, via runtimes such as wasmtime or WASI), with near-native performance. This in particular allows to run the same code (or close to it) as one would run locally in the terminal for doing bioinformatics analyses, in a website under whichever platform. This also allows to use languages such as Rust to bear the burden of the most computing-demanding part of the algorithms, instead of using JavaScript, that might be slower for those.

Another main feature from wasm is that allows for running methods fully locally. With this compilation target, as the exact same code could be potentially run with it, no upload of input data (e.g. a genome) to some remote server is needed, as many other websites offer, the same goes for downloading the results. This makes it ideally for resource-constrained environments (in particular, bad Internet connection), as well as for working with sensitive data, as it does not need to leave your computer.

From the browser’s perspective, a wasm module is essentially a well-defined binary blob that exports functions and, optionally, memory. JavaScript loads the module, instantiates it, and calls its exports as ordinary function calls. There is no subprocess, no file system access by default, and no shared state beyond what the module explicitly exposes.


Available Targets

Rust expresses compilation targets using the standard <arch>-<vendor>-<os> triple. The most relevant wasm targets for us are:

TargetStatus in RustNotes
wasm32-unknown-unknownTier 2No OS abstraction; the standard target for browser-facing wasm. Used by this project.
wasm64-unknown-unknownTier 364-bit address space (Memory64 proposal); support in wasm-bindgen and wasm-pack is in active development but not yet released (see below).

For browser-facing Rust code combined with wasm-bindgen and wasm-pack, wasm32-unknown-unknown is the only practical choice. It provides no operating system layer whatsoever — no file I/O, no threading primitives, no environment variables — which is both its strength (minimal, portable binary) and a source of constraints discussed in the Limitations section.

A note on wasm64

The wasm64-unknown-unknown target, corresponding to the WebAssembly Memory64 proposal, would allow modules to address more than 4 GiB of linear memory. This is potentially very relevant for bioinformatics tools operating on large reference genomes or graphs. However, as of the time of writing, toolchain support is still in progress and has not yet landed in a stable release:

  • wasm-bindgen: PR #5004 — “Add wasm64/memory64 support for wasm64-unknown-unknown target” — is open and pending review. It was filed in February 2026 and closes the two underlying tracking issues (#4436 and #4499).
  • wasm-pack: open issue #1464, with a companion PR #1553 linked from the wasm-bindgen PR above.

Until both PRs are merged and released, wasm32-unknown-unknown remains the only usable target for browser-facing Rust with wasm-pack. Memory-intensive computations must be structured to fit within the 4 GiB linear memory limit, and the interface between Rust and JavaScript should be designed accordingly.


Toolchain

Install the following tools before starting:

# Install the wasm target for the Rust standard library
rustup target add wasm32-unknown-unknown

# Install wasm-pack — handles compilation, wasm-bindgen, and JS/TS glue generation
cargo install wasm-pack

The central tool is wasm-pack. It orchestrates, so that you don’t need to take care of this:

  1. Calling cargo build --target wasm32-unknown-unknown.
  2. Running wasm-bindgen to generate the JavaScript/TypeScript glue layer.
  3. Optionally running wasm-opt for further binary optimisation.
  4. Producing a pkg/ directory containing the .wasm file, the generated JS module, and a TypeScript declaration file.

wasm-bindgen is the lower-level library that makes it possible to annotate Rust types and functions so that they can be called from JavaScript with idiomatic types (strings, typed arrays, JS objects) rather than raw integer pointers.


Workflow

The overall final way of running Rust code with WebAssembly in your website will look like this:

┌─────────────────────────────────────────────────────────────────┐
│  Rust library crate (crate-type = ["cdylib"])                   │
│                                                                 │
│  1. Annotate public API with #[wasm_bindgen]                    │
│  2. Ensure that it compiles for your wasm target                │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│  Web front end (JavaScript / TypeScript / Vue / …)              │
│                                                                 │
│  3. Use wasm-pack to automatically compile, bind (wasm-bindgen),│
│     optimise (wasm-opt), and put the binaries in a folder in    │
│     your web repository                                         │
│  4. Initialise the wasm module (async, one-time)                │
│  5. Call exported Rust functions or construct exported structs  │
│  6. Receive results as JS-native types                          │
└─────────────────────────────────────────────────────────────────┘

Designing the Rust interface

Cargo.toml setup

The crate must be declared as a cdylib (C-compatible dynamic library), which is the form wasm modules take:

[lib]
crate-type = ["cdylib"]

[dependencies]
### Whatever version you want/need, here 0.2 is an example
wasm-bindgen = "0.2"

# For returning errors to JS:
# wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }

If you want, you can also declare it as rlib in the same crate-type variable in the toml file.

Annotating with #[wasm_bindgen]

The #[wasm_bindgen] attribute is the bridge between Rust and JavaScript. It can annotate free functions or impl blocks on structs.

Option A — Free functions (simplest, good for stateless operations):

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn gc_content(sequence: &str) -> f64 {
    let len = sequence.len() as f64;
    if len == 0.0 {
        return 0.0;
    }
    let gc = sequence
        .bytes()
        .filter(|&b| matches!(b, b'G' | b'C' | b'g' | b'c'))
        .count() as f64;
    gc / len
}
}

Option B — Exported struct with methods (better for stateful objects, e.g., a k-mer index or an assembler graph that must persist across multiple JS calls):

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct KmerCounter {
    counts: std::collections::HashMap<String, usize>,
    k: usize,
}

// In this example, all these methods will become accessible to JavaScript code
#[wasm_bindgen]
impl KmerCounter {
    // Here we re-declare this particular method with #[wasm_bindgen(constructor)],
    // as this will allow us to later, in JavaScript, just create an instance of this
    // object, exactly as if it was a normal instance in JavaScript.
    #[wasm_bindgen(constructor)]
    pub fn new(k: usize) -> Self {
        Self {
            counts: std::collections::HashMap::new(),
            k,
        }
    }

    pub fn add_sequence(&mut self, sequence: &str) {
        for i in 0..sequence.len().saturating_sub(self.k - 1) {
            let kmer = &sequence[i..i + self.k];
            *self.counts.entry(kmer.to_string()).or_insert(0) += 1;
        }
    }

    pub fn count(&self, kmer: &str) -> usize {
        *self.counts.get(kmer).unwrap_or(&0)
    }

    pub fn total_kmers(&self) -> usize {
        self.counts.values().sum()
    }
}
}

Passing data across the boundary

The types that wasm-bindgen handles transparently include: bool, i32/u32/i64/u64/f32/f64, String/&str, Vec<u8>/&[u8] (as Uint8Array in JS), and JsValue. For returning richer structured data (e.g., multiple fields), consider:

  • Returning a JsValue built with serde_wasm_bindgen (requires the serde feature).
  • Using multiple getter methods on an exported struct.
  • Encoding the result as JSON and returning it as a String.

Avoid passing very large strings or byte slices across the boundary on every call; instead, pass them once to a stateful object and keep results on the Rust side until they are needed.

Conditional compilation

It is good practice to gate wasm-bindgen imports behind a cfg attribute so that the crate still compiles and can be tested natively:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
}

This is also a way to have, in the same crate/repository, code that compiles to x86_64, and also to WebAssembly, as you can essentially create alternative “versions” of those methods, functions, or crates that need to be updated.


Building and bundling, automatically

In sparrowhawk, we use the @wasm-tool/wasm-pack-plugin to integrate WASM compilation directly into the Vue build process. This approach automatically handles compilation, binding generation, and module integration.

Vue Configuration with wasm-pack-plugin

The vue.config.js file configures the wasm-pack-plugin for each Rust crate:

const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const path = require("path");

module.exports = {
    configureWebpack: {
        experiments: {
            asyncWebAssembly: true,
        },
    },
    chainWebpack: (config) => {
        // Configure wasm-pack plugin for your Rust crate
        config
            .plugin("wasm-pack_your_crate")
            .use(WasmPackPlugin)
            .init(
                (Plugin) =>
                    new Plugin({
                        crateDirectory: path.resolve(__dirname, "../rust/your-crate"),
                        outDir: path.resolve(__dirname, "./src/pkg"),
                        forceMode: "production",
                    })
            )
            .end()
    },
}

Key Configuration Options

  • crateDirectory: Path to your Rust crate directory
  • outDir: Output directory for generated files (relative to Vue project)
  • forceMode: Set to "production" for optimized builds

Build Process

When you run npm run build or npm run serve, the wasm-pack-plugin automatically:

  1. Compiles your Rust crate to WASM using wasm32-unknown-unknown target
  2. Runs wasm-bindgen to generate JavaScript/TypeScript glue code
  3. Optimizes the WASM binary with wasm-opt
  4. Places the output files in the specified outDir

The output in src/pkg/ will contain:

  • <crate_name>_bg.wasm — the compiled WASM binary
  • <crate_name>.js — the JavaScript glue module
  • <crate_name>.d.ts — TypeScript declarations
  • package.json — npm package metadata

Manual Build (Alternative)

If you need to build manually outside the Vue build process:

# Navigate to your Rust crate directory
cd ../rust/your-crate

# Build with wasm-pack
wasm-pack build --target web --release --no-default-features --features wasm

# Copy the pkg directory to your Vue project
cp -r pkg ../web/www/src/pkg

Note: The manual approach requires you to manually copy files and may not integrate as smoothly with the Vue development server.

Web integration

One simple way of dealing with your Rust code in your website is through web workers: these are, in essence, independent threads that you can focus on simply talking to your WebAssembly interface.

Architecture overview

┌─────────────────────────────────────────────────────────────────┐
│  Web (Vue) components (main thread)                             │
│                                                                 │
│  1. User interacts with UI                                      │
│  2. Component creates Worker                                    │
│  3. Sends data to Worker via postMessage                        │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│  Web Worker (background thread)                                 │
│                                                                 │
│  4. Worker dynamically imports WASM module                      │
│  5. Calls WASM functions                                        │
│  6. Processes results                                           │
│  7. Sends results back to main thread                           │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│  WASM module (Rust compiled to WebAssembly)                     │
│                                                                 │
│  8. Executes bioinformatics algorithms                          │
│  9. Returns computed results to the worker                      │
└─────────────────────────────────────────────────────────────────┘

Worker class implementation

Here’s a more complete example showing how you can write your web workers. You will create usually two files, one with the actual class definition, and other with how to deal with the messages that you will exchange between it and the main thread that runs your website. Let’s go with the first file (class declaration):

// src/workers/BioWorker.ts
interface WasmModule {
  // Define your WASM module interface
  process_sequence: (sequence: string, params: any) => string;
  // Add other exported functions and classes
}

type WasmModuleAny = any;

export class BioWorker {
  private worker: Worker;
  private wasm: WasmModuleAny | null = null;
  private wasmPromise: Promise<WasmModuleAny>;
  
  constructor() {
    this.worker = new Worker(new URL('./bio-worker.impl.ts', import.meta.url));
    this.wasm = null;
    
    this.wasmPromise = new Promise((resolve) => {
      this.worker.onmessage = (event) => {
        if (event.data.type === 'wasm_loaded') {
          this.wasm = event.data.wasm;
          resolve(this.wasm);
        }
      };
    });
  }
  
  private async waitForWasm(): Promise<WasmModuleAny> {
    return this.wasm ? Promise.resolve(this.wasm) : this.wasmPromise;
  }
  
  public async processData(sequence: string, params: any): Promise<string> {
    await this.waitForWasm();
    
    return new Promise((resolve, reject) => {
      const messageId = Math.random().toString(36).substring(2);
      
      this.worker.postMessage({
        type: 'process',
        messageId,
        sequence,
        params
      });
      
      const handler = (event: MessageEvent) => {
        if (event.data.messageId === messageId) {
          this.worker.removeEventListener('message', handler);
          if (event.data.error) {
            reject(event.data.error);
          } else {
            resolve(event.data.result);
          }
        }
      };
      
      this.worker.addEventListener('message', handler);
    });
  }
  
  public terminate() {
    this.worker.terminate();
  }
}

And now, the second file:

// src/workers/bio-worker.impl.ts
interface WasmModule {
  process_sequence: (sequence: string, params: any) => string;
  init?: () => Promise<void>;
}

type WasmModuleAny = any;

let wasm: WasmModuleAny | null = null;

self.onmessage = async (event) => {
  try {
    switch (event.data.type) {
      case 'init':
        // Load WASM module
        wasm = await import('@/pkg');
        if (wasm.init) {
          await wasm.init();
        }
        self.postMessage({ type: 'wasm_loaded', wasm });
        break;
        
      case 'process':
        if (!wasm) {
          throw new Error('WASM not initialized');
        }
        
        // Process the sequence with WASM
        const result = wasm.process_sequence(event.data.sequence, event.data.params);
        
        self.postMessage({
          messageId: event.data.messageId,
          result: result
        });
        break;
    }
  } catch (error) {
    console.error('Worker error:', error);
    self.postMessage({
      messageId: event.data?.messageId,
      error: error.message
    });
  }
};

Using the Worker in Vue Components

And here you can see and example of how to call the previous worker example in a web (a Vue “component”).

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
import { BioWorker } from '@/workers/BioWorker';

export default defineComponent({
  name: 'BioProcessing',
  setup() {
    const result = ref('');
    const isProcessing = ref(false);
    const bioWorker = ref<BioWorker | null>(null);
    
    onMounted(() => {
      // Initialize worker when component mounts
      bioWorker.value = new BioWorker();
    });
    
    onUnmounted(() => {
      // Clean up worker when component unmounts
      bioWorker.value?.terminate();
    });
    
    const processSequence = async (sequence: string) => {
      if (!bioWorker.value) return;
      
      isProcessing.value = true;
      try {
        const processed = await bioWorker.value.processData(sequence, {
          // Add processing parameters
        });
        result.value = processed;
      } catch (error) {
        result.value = `Error: ${error.message}`;
      } finally {
        isProcessing.value = false;
      }
    };
    
    return { result, isProcessing, processSequence };
  }
});
</script>

Benefits of the worker pattern

  1. Non-blocking UI: Computationally intensive operations don’t freeze the user interface
  2. Better memory management: Workers can be terminated to clean up memory
  3. Reusability: Worker logic can be shared across multiple components
  4. Error isolation: Errors in workers don’t crash the main application
  5. Performance: Multiple workers can run in parallel (within browser limits)

Limitations

WebAssembly in the browser is a capable environment, but it imposes real constraints that are particularly relevant for bioinformatics tools.

Memory cap

wasm32 modules have a maximum linear memory of 4 GiB. This is a hard constraint for tools that load large reference genomes or full-resolution pangenome graphs into memory. Strategies include streaming and chunked processing, or restricting input sizes in the web interface. The forthcoming wasm64 target would lift this restriction, and toolchain support is actively being developed (see Available Targets), but it has not yet landed in a stable release of the Rust/Web interfacing libraries we are considering in this guide.

Call stack depth

The wasm call stack is much shallower than a typical native stack. Deeply recursive algorithms may overflow the stack at runtime. Rewrite critical paths iteratively or increase the stack size at link time (with a .cargo/config.toml entry such as [target.wasm32-unknown-unknown] rustflags = ["-C", "link-args=-z stack-size=<bytes>"]), accepting the trade-off of increased module size.

Incompatible crates

Not every Rust crate compiles to wasm32-unknown-unknown. Crates that depend on OS-level facilities — file I/O, threads, system clocks, native dynamic linking, or C libraries via cc/cmake — will fail to compile. A notable example in bioinformatics is needletail, which cannot currently be compiled to this target. When porting an existing tool, audit your dependency tree with:

cargo tree --target wasm32-unknown-unknown 2>&1 | grep -E "error|could not"

Common workarounds: replace the offending crate with a pure-Rust alternative; re-implement only the required subset; or gate the incompatible dependency behind #[cfg(not(target_arch = "wasm32"))] and providing a stub for the wasm build.

SIMD and advanced CPU instructions

The WebAssembly SIMD proposal (wasm32 SIMD128) is now widely supported in major browsers, but it is not the same instruction set as x86 SSE/AVX or ARM NEON. Code that relies on platform-specific SIMD intrinsics (e.g., via the packed_simd or std::arch crates targeting x86) will not compile. You can, however, use portable SIMD via the std::simd API (nightly) or crates such as wide, which will lower to SIMD128 when targeting wasm. Performance from SIMD128 is typically lower than from AVX2/AVX-512 on a desktop CPU.

No multithreading (by default)

True threading in wasm requires SharedArrayBuffer, which is behind cross-origin isolation headers and not universally straightforward to configure. The rayon crate does not work out of the box in wasm32-unknown-unknown. For parallel workloads, consider Web Workers at the JavaScript level, passing disjoint chunks of data to separate wasm instances. Sharing data at this level is possible with shared memory objects, although it will apply the overall limit of 4MB.

Performance expectations

WebAssembly can achieve 60–90 % of native performance for well-optimised, compute-bound code. However, memory-intensive code (with many small allocations or frequent cache misses) and code with a high wasm/JS boundary-crossing frequency can perform significantly worse. Profile the bottlenecks before assuming a native algorithm translates directly.

No filesystem

wasm32-unknown-unknown has no access to the host filesystem. Input data must be passed from JavaScript as byte slices or strings (e.g., from a <input type="file"> element read by the browser’s FileReader/File API). This is a deliberate design constraint of the target, not a bug.


A note on WebGPU

For computations that would benefit from GPU parallelism, WebGPU is the browser’s emerging standard for general-purpose GPU computation (GPGPU). It is accessible from JavaScript and can be combined with a wasm module: the wasm code handles data preparation and result parsing, while dispatch and shader execution are managed through the WebGPU JavaScript API.

From Rust, the wgpu crate provides a cross-platform WebGPU abstraction that compiles to wasm32-unknown-unknown (targeting the browser’s WebGPU backend). This requires writing WGSL shaders and managing GPU buffer lifetimes, but it opens the door to genuinely GPU-accelerated methods in the browser. WebGPU is currently supported in Chrome and (behind a flag) in Firefox; Safari support is partial. Browser compatibility should be verified before committing to a GPU-dependent architecture.


Minimum working example (MWE)

The following self-contained example computes the GC content of a DNA sequence entered by the user. It mirrors the structure used in this repository: a small Rust library compiled with wasm-pack, called from a single HTML file with plain JavaScript.

Project layout

gc-wasm/
├── Cargo.toml
├── src/
│   └── lib.rs
└── www/
    ├── public/
    │   └── index.html
    ├── src/
    │   ├── components/
    │   │   └── GCContent.vue
    │   ├── workers/
    │   │   ├── gc.worker.ts
    │   │   └── GCWorker.ts
    │   ├── App.vue
    │   ├── main.ts
    │   └── shims-vue.d.ts
    ├── vue.config.js
    ├── package.json
    └── tsconfig.json

Cargo.toml

[package]
name = "gc-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

src/lib.rs

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

/// Computes the GC content of a DNA sequence as a value in [0, 1].
/// Non-ACGT characters are counted in the denominator but not the numerator,
/// so ambiguous bases dilute the GC fraction rather than being ignored.
#[wasm_bindgen]
pub fn gc_content(sequence: &str) -> f64 {
    let total = sequence.len();
    if total == 0 {
        return 0.0;
    }
    let gc = sequence
        .bytes()
        .filter(|&b| matches!(b, b'G' | b'C' | b'g' | b'c'))
        .count();
    gc as f64 / total as f64
}

/// Returns the length of the sequence after stripping whitespace and
/// newlines — useful when the user pastes a FASTA body.
#[wasm_bindgen]
pub fn clean_length(sequence: &str) -> usize {
    sequence
        .bytes()
        .filter(|b| !b.is_ascii_whitespace())
        .count()
}
}

Vue integration

Instead of the manual build approach, integrate the WASM module with Vue using the wasm-pack-plugin:

Vue configuration (vue.config.js)

const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const path = require("path");

module.exports = {
    configureWebpack: {
        experiments: {
            asyncWebAssembly: true,
        },
    },
    chainWebpack: (config) => {
        config
            .plugin("wasm-pack_gc")
            .use(WasmPackPlugin)
            .init(
                (Plugin) =>
                    new Plugin({
                        crateDirectory: path.resolve(__dirname, "../gc-wasm"),
                        outDir: path.resolve(__dirname, "./src/pkg"),
                        forceMode: "production",
                    })
            )
            .end()
    },
}

Vue component (src/components/GCContent.vue)

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
import { GCWorker } from '@/workers/GCWorker';

export default defineComponent({
  name: 'GCContent',
  setup() {
    const sequence = ref('');
    const result = ref('');
    const isLoading = ref(false);
    const gcWorker = ref<GCWorker | null>(null);
    
    // Initialize worker when component mounts
    onMounted(() => {
      gcWorker.value = new GCWorker();
    });
    
    // Clean up worker when component unmounts
    onUnmounted(() => {
      gcWorker.value?.terminate();
    });
    
    const calculate = async () => {
      if (!sequence.value.trim()) {
        result.value = 'Please enter a sequence';
        return;
      }
      
      if (!gcWorker.value) {
        result.value = 'Worker not initialized';
        return;
      }
      
      isLoading.value = true;
      try {
        // Clean sequence (remove whitespace and FASTA headers)
        const cleanSeq = sequence.value
          .split('\n')
          .filter(line => !line.startsWith('>'))
          .join('')
          .replace(/\s+/g, '');
        
        if (cleanSeq.length === 0) {
          result.value = 'No valid sequence found';
          return;
        }
        
        // Call WASM functions through worker
        const gc = await gcWorker.value.calculateGC(cleanSeq);
        const length = cleanSeq.length;
        
        result.value =
          `Length: ${length} bp\n` +
          `GC: ${(gc * 100).toFixed(2)}%`;
          
      } catch (error) {
        console.error('WASM worker error:', error);
        result.value = `Error: ${error.message}`;
      } finally {
        isLoading.value = false;
      }
    };
    
    return { sequence, result, isLoading, calculate };
  }
});
</script>

<template>
  <div class="gc-calculator">
    <h2>GC Content Calculator</h2>
    <p>Paste a DNA sequence (FASTA body or raw bases):</p>
    <textarea 
      v-model="sequence" 
      placeholder="ATGCGCATGCTTAAGC... or FASTA format"
      rows="6"
      class="w-full p-2 border rounded"
    ></textarea>
    <button 
      @click="calculate" 
      :disabled="isLoading"
      class="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-300"
    >
      {{ isLoading ? 'Calculating...' : 'Calculate' }}
    </button>
    <div class="mt-4 p-2 bg-gray-100 rounded font-mono whitespace-pre">
      {{ result }}
    </div>
  </div>
</template>

<style scoped>
.gc-calculator {
  max-width: 600px;
  margin: 0 auto;
  font-family: sans-serif;
}
textarea {
  font-family: monospace;
  width: 100%;
}
</style>

Worker files

Worker implementation (src/workers/gc.worker.ts)
// Handle messages from main thread
self.onmessage = async (event) => {
  try {
    // Dynamically import the WASM module
    const wasm = await import('@/pkg');
    
    // Call the WASM function
    const result = wasm.gc_content(event.data.sequence);
    
    // Send result back to main thread
    self.postMessage({ result });
  } catch (error) {
    console.error('WASM error in worker:', error);
    self.postMessage({ error: error.message });
  }
};
Worker class (src/workers/GCWorker.ts)
// Worker class that manages the web worker lifecycle
export class GCWorker {
  private worker: Worker;
  
  constructor() {
    // Create worker instance
    this.worker = new Worker(new URL('./gc.worker.ts', import.meta.url));
  }
  
  public calculateGC(sequence: string): Promise<number> {
    return new Promise((resolve, reject) => {
      // Generate unique message ID for this request
      const messageId = Math.random().toString(36).substring(2);
      
      // Message handler for this specific request
      const handler = (event: MessageEvent) => {
        if (event.data.messageId === messageId) {
          this.worker.removeEventListener('message', handler);
          if (event.data.result !== undefined) {
            resolve(event.data.result);
          } else if (event.data.error) {
            reject(new Error(event.data.error));
          }
        }
      };
      
      this.worker.addEventListener('message', handler);
      
      // Send sequence to worker
      this.worker.postMessage({
        messageId,
        sequence
      });
    });
  }
  
  public terminate() {
    // Clean up worker when no longer needed
    this.worker.terminate();
  }
}

www/index.html

=======

Place this file inside the www/ directory, and serve both www/ and pkg/ from the same origin (e.g., with python3 -m http.server from the gc-wasm/ directory, adjusting the import path accordingly, or by copying pkg/ into www/pkg/).

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>GC Content Calculator</title>
  <style>
    body      { font-family: monospace; max-width: 640px; margin: 2rem auto; }
    textarea  { width: 100%; height: 8rem; font-family: monospace; }
    output    { display: block; margin-top: 1rem; font-size: 1.2rem; }
    .error    { color: #c00; }
  </style>
</head>
<body>
  <h1>GC Content Calculator</h1>
  <p>Paste a DNA sequence (FASTA body or raw bases):</p>
  <textarea id="seq" placeholder="ATGCGCATGCTTAAGC..."></textarea>
  <button id="calc">Calculate</button>
  <output id="result"></output>

  <script type="module">
    // Adjust the path if pkg/ lives elsewhere relative to this file.
    import init, { gc_content, clean_length } from "../pkg/gc_wasm.js";

    // Initialise the wasm module once, before any calls.
    await init();

    document.getElementById("calc").addEventListener("click", () => {
      const raw   = document.getElementById("seq").value;
      const out   = document.getElementById("result");

      if (!raw.trim()) {
        out.textContent = "";
        return;
      }

      // Strip FASTA header lines if present.
      const body = raw
        .split("\n")
        .filter(line => !line.startsWith(">"))
        .join("");

      const len = clean_length(body);
      const gc  = gc_content(body);

      out.textContent =
        `Length : ${len} bp\n` +
        `GC     : ${(gc * 100).toFixed(2)} %`;
    });
  </script>
</body>
</html>

Serving locally

# Navigate to the Vue project directory
cd www

# Install dependencies
npm install

# Start development server
npm run serve

# The Vue CLI development server will start and show the URL
# Typically http://localhost:8080
# Open this URL in your browser

Note: The Vue CLI development server handles all the necessary configuration for WASM module loading. Unlike the Python HTTP server approach, it provides hot module replacement, proper asset handling, and automatic reloading during development.

Building for production

# Create optimized production build
npm run build

# The built files will be in www/dist/ directory
# You can serve these with any static file server

www/package.json

{
  "name": "gc-wasm-vue",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "vue": "^3.2.13",
    "core-js": "^3.8.3"
  },
  "devDependencies": {
    "@vue/cli-service": "^5.0.0",
    "@wasm-tool/wasm-pack-plugin": "^1.0.0",
    "typescript": "^4.5.0",
    "@vue/compiler-sfc": "^3.2.13"
  }
}

Further Reading