A code size profiler for Wasm

Guide | Contributing | Chat

Built with ๐Ÿฆ€๐Ÿ•ธ by The Rust and WebAssembly Working Group


Twiggy is a code size profiler for Wasm. It analyzes a binary's call graph to answer questions like:

  • Why was this function included in the binary in the first place? Who calls it?

  • What is the retained size of this function? I.e. how much space would be saved if I removed it and all the functions that become dead code after its removal.

Use Twiggy to make your binaries slim!

๐Ÿ“ฆ Install

Ensure that you have the Rust toolchain installed, then run:

cargo install twiggy

๐Ÿ’ก Concepts

This section provides some background knowledge on concepts that are useful to understand when using Twiggy.

Call Graph

Consider the following functions:

fn main() {
pub fn shred() {

fn gnar_gnar() {

fn bluebird() {

fn weather_report() {

fn pow() {

fn fluffy() {}

fn soft() {}

pub fn baker() {

fn hood() {}

If we treat every function as a vertex in a graph, and if we add an edge from A to B if function A calls function B, then we get the following call graph:

Call Graph


If there is a path where A โ†’ B โ†’ ... โ†’ C through the call graph, then we say that C is reachable through from A. Dead code is code that is not reachable in the call graph from any publicly exported functions (for libraries) or the main function (for executables).

Recall our example call graph:

Call Graph

Imagine that shred was our executable's main function. In this scenario, there is no path through the call graph from shred to baker or hood, so they are dead code. We would expect that the linker would remove them, and they wouldn't show up in the final binary.

But what if some function that you thought was dead code is appearing inside your binary? Maybe it is deep down in some library you depend on, but inside a submodule of that library that you aren't using, and you wouldn't expect it to be included in the final binary.

In this scenario, twiggy can show you all the paths in the call graph that lead to the unexpected function. This lets you understand why the unwelcome function is present, and decide what you can do about it. Maybe if you refactored your code to avoid calling Y, then there wouldn't be any paths to the unwelcome function anymore, it would be dead code, and the linker would remove it.

Dominators and Retained Size

Let's continue to use this example call graph:

Call Graph

Imagine the pow function itself might is not very large. But it calls functions soft and fluffy, both of which are huge. And they are both only called by pow, so if pow were removed, then soft and fluffy would both become dead code and get removed as well. Therefore, pow's "real" size is huge, even though it doesn't look like it at a glance.

The dominator relationship gives us a way to reason about the retained size of a function.

In a graph that is rooted at vertex R, vertex A is said to dominate vertex B if every path in the graph from R to B includes A. It follows that if A were removed from the graph, then B would become unreachable.

In our call graphs, the roots are the main function (for executables) or publicly exported functions (for libraries).

V is the immediate dominator of a vertex U if V != U, and there does not exist another distinct vertex W that is dominated by V but also dominates U. If we take all the vertices from a graph, remove the edges, and then add edges for each immediate dominator relationship, then we get a tree. Here is the dominator tree for our call graph from earlier, where shred is the root:

Dominator Tree

Using the dominator relationship, we can find the retained size of some function by taking its shallow size and adding the retained sizes of each function that it immediately dominates.

Generic Functions and Monomorphization

Generic functions with type parameters in Rust and template functions in C++ can lead to code bloat if you aren't careful. Every time you instantiate these generic functions with a concrete set of types, the compiler will monomorphize the function, creating a copy of its body replacing its generic placeholders with the specific operations that apply to the concrete types. This presents many opportunities for compiler optimizations based on which particular concrete types each copy of the function is working with, but these copies add up quickly in terms of code size.

Example of monomorphization in Rust:

fn main() {
fn generic_function<T: MyTrait>(t: T) { ... }

// Each of these will generate a new copy of `generic_function`!

Example of monomorphization in C++:

template<typename T>
void generic_function(T t) { ... }

// Each of these will also generate a new copy of `generic_function`!

If you can afford the runtime cost of dynamic dispatch, then changing these functions to use trait objects in Rust or virtual methods in C++ can likely save a significant amounts of code size. With dynamic dispatch, the generic function's body is not copied, and the generic bits within the function become indirect function calls.

Example of dynamic dispatch in Rust:

fn main() {
fn generic_function(t: &MyTrait) { ... }
// or
fn generic_function(t: Box<MyTrait>) { ... }
// etc...

// No more code bloat!
let x = MyTraitImpl::new();
let y = AnotherMyTraitImpl::new();
let z = MyTraitImplAlso::new();

Example of dynamic dispatch in C++:

class GenericBase {
    virtual void generic_impl() = 0;

class MyThing : public GenericBase {
    virtual void generic_impl() override { ... }

class AnotherThing : public GenericBase {
    virtual void generic_impl() override { ... }

class AlsoThing : public GenericBase {
    virtual void generic_impl() override { ... }

void generic(GenericBase& thing) { ... }

// No more code bloat!
MyThing x;
AnotherThing y;
AlsoThing z;

twiggy can analyze a binary to find which generic functions are being monomorphized repeatedly, and calculate an estimation of how much code size could be saved by switching from monomorphization to dynamic dispatch.

๐Ÿ‹๏ธโ€โ™€๏ธ Usage

Twiggy is primarily a command line tool, but it can also be used as a library crate from within other Rust projects, or compiled to WebAssembly and used from JavaScript on the Web or from Node.js

โŒจ Command Line Interface

twiggy is primarily a command line tool.

To get the most up-to-date usage for the version of twiggy that you have installed, you can always run:

twiggy --help

Or, to get more information about a sub-command, run:

twiggy subcmd --help

twiggy top

The twiggy top sub-command summarizes and lists the top code size offenders in a binary.

 Shallow Bytes โ”‚ Shallow % โ”‚ Item
          1034 โ”Š    36.71% โ”Š data[3]
           777 โ”Š    27.58% โ”Š "function names" subsection
           226 โ”Š     8.02% โ”Š wee_alloc::alloc_first_fit::h9a72de3af77ef93f
           165 โ”Š     5.86% โ”Š hello
           153 โ”Š     5.43% โ”Š wee_alloc::alloc_with_refill::hb32c1bbce9ebda8e
           137 โ”Š     4.86% โ”Š <wee_alloc::size_classes::SizeClassAllocPolicy<'a> as wee_alloc::AllocPolicy>::new_cell_for_free_list::h3987e3054b8224e6
            77 โ”Š     2.73% โ”Š <wee_alloc::LargeAllocPolicy as wee_alloc::AllocPolicy>::new_cell_for_free_list::h8f071b7bce0301ba
            45 โ”Š     1.60% โ”Š goodbye
            25 โ”Š     0.89% โ”Š data[1]
            25 โ”Š     0.89% โ”Š data[2]
           153 โ”Š     5.43% โ”Š ... and 27 more.
          2817 โ”Š   100.00% โ”Š ฮฃ [37 Total Rows]

twiggy paths

The twiggy paths sub-command finds the call paths to a function in the given binary's call graph. This tells you what other functions are calling this function, why this function is not dead code, and therefore why it wasn't removed by the linker.

 Shallow Bytes โ”‚ Shallow % โ”‚ Retaining Paths
           153 โ”Š     5.43% โ”Š wee_alloc::alloc_with_refill::hb32c1bbce9ebda8e
               โ”Š           โ”Š   โฌ‘ <wee_alloc::size_classes::SizeClassAllocPolicy<'a> as wee_alloc::AllocPolicy>::new_cell_for_free_list::h3987e3054b8224e6
               โ”Š           โ”Š       โฌ‘ elem[0]
               โ”Š           โ”Š           โฌ‘ table[0]
               โ”Š           โ”Š   โฌ‘ hello
               โ”Š           โ”Š       โฌ‘ export "hello"

twiggy monos

The twiggy monos sub-command lists the generic function monomorphizations that are contributing to code bloat.

 Apprx. Bloat Bytes โ”‚ Apprx. Bloat % โ”‚ Bytes โ”‚ %      โ”‚ Monomorphizations
               2141 โ”Š          3.68% โ”Š  3249 โ”Š  5.58% โ”Š alloc::slice::merge_sort
                    โ”Š                โ”Š  1108 โ”Š  1.90% โ”Š     alloc::slice::merge_sort::hb3d195f9800bdad6
                    โ”Š                โ”Š  1108 โ”Š  1.90% โ”Š     alloc::slice::merge_sort::hfcf2318d7dc71d03
                    โ”Š                โ”Š  1033 โ”Š  1.77% โ”Š     alloc::slice::merge_sort::hcfca67f5c75a52ef
               1457 โ”Š          2.50% โ”Š  4223 โ”Š  7.26% โ”Š <&'a T as core::fmt::Debug>::fmt
                    โ”Š                โ”Š  2766 โ”Š  4.75% โ”Š     <&'a T as core::fmt::Debug>::fmt::h1c27955d8de3ff17
                    โ”Š                โ”Š   636 โ”Š  1.09% โ”Š     <&'a T as core::fmt::Debug>::fmt::hea6a77c4dcddb7ac
                    โ”Š                โ”Š   481 โ”Š  0.83% โ”Š     <&'a T as core::fmt::Debug>::fmt::hfbacf6f5c9f53bb2
                    โ”Š                โ”Š   340 โ”Š  0.58% โ”Š     ... and 1 more.
               3759 โ”Š          6.46% โ”Š 31160 โ”Š 53.54% โ”Š ... and 214 more.
               7357 โ”Š         12.64% โ”Š 38632 โ”Š 66.37% โ”Š ฮฃ [223 Total Rows]

twiggy dominators

The twiggy dominators sub-command displays the dominator tree of a binary's call graph.

 Retained Bytes โ”‚ Retained % โ”‚ Dominator Tree
         175726 โ”Š     14.99% โ”Š export "items_parse"
         175712 โ”Š     14.98% โ”Š   โคท items_parse
         131407 โ”Š     11.21% โ”Š       โคท twiggy_parser::wasm_parse::<impl twiggy_parser::Parse for wasmparser::readers::module::ModuleReader>::parse_items::h39c45381d868d181
          18492 โ”Š      1.58% โ”Š       โคท wasmparser::binary_reader::BinaryReader::read_operator::hb1c7cde18e148939
           2677 โ”Š      0.23% โ”Š       โคท alloc::collections::btree::map::BTreeMap<K,V>::insert::hd2463626e5ac3441
           1349 โ”Š      0.12% โ”Š       โคท wasmparser::readers::module::ModuleReader::read::hb76af8efd547784f
           1081 โ”Š      0.09% โ”Š       โคท core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &mut F>::call_once::h1ff7fe5b944492c3
            776 โ”Š      0.07% โ”Š       โคท <wasmparser::readers::import_section::ImportSectionReader as wasmparser::readers::section_reader::SectionReader>::read::h12903e6d8d4091bd

twiggy diff

The twiggy diff sub-command computes the delta size of each item between old and new versions of a binary.

 Delta Bytes โ”‚ Item
       -1034 โ”Š data[3]
        -593 โ”Š "function names" subsection
        +396 โ”Š wee_alloc::alloc_first_fit::he2a4ddf96981c0ce
        +243 โ”Š goodbye
        -226 โ”Š wee_alloc::alloc_first_fit::h9a72de3af77ef93f
        -262 โ”Š ... and 29 more.
       -1476 โ”Š ฮฃ [34 Total Rows]

twiggy garbage

The twiggy garbage sub-command finds and displays dead code and data that is not transitively referenced by any exports or public functions.

 Bytes โ”‚ Size % โ”‚ Garbage Item
    12 โ”Š  6.09% โ”Š unusedAddThreeNumbers
     9 โ”Š  4.57% โ”Š unusedAddOne
     7 โ”Š  3.55% โ”Š type[2]: (i32, i32, i32) -> i32
     6 โ”Š  3.05% โ”Š unusedChild
     5 โ”Š  2.54% โ”Š type[1]: (i32) -> i32
     4 โ”Š  2.03% โ”Š type[0]: () -> i32
    43 โ”Š 21.83% โ”Š ฮฃ [6 Total Rows]

๐Ÿฆ€ As a Crate

twiggy is divided into a collection of crates that you can use programmatically, but no long-term stability is promised. We will follow semver as best as we can, but will err on the side of being more conservative with breaking version bumps than might be strictly necessary.

Here is a simple example:

extern crate twiggy_analyze;
extern crate twiggy_opt;
extern crate twiggy_parser;

use std::fs;
use std::io;

fn main() {
    let mut file = fs::File::open("path/to/some/binary").unwrap();
    let mut data = vec![];
    file.read_to_end(&mut data).unwrap();

    let items = twiggy_parser::parse(&data).unwrap();

    let options = twiggy_opt::Top::default();
    let top = twiggy_analyze::top(&mut items, &options).unwrap();

    let mut stdout = io::stdout();
    top.emit_text(&items, &mut stdout).unwrap();

For a more in-depth example, take a look at the implementation of the twiggy CLI crate.

๐Ÿ•ธ On the Web with WebAssembly

First, ensure you have the wasm32-unknown-unknown Rust target installed and up-to-date:

rustup target add wasm32-unknown-unknown

Next, install wasm-bindgen:

cargo install wasm-bindgen-cli

Finally, build twiggy's WebAssembly API with wasm-bindgen:

cd twiggy/wasm-api
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen ../target/wasm32-unknown-unknown/release/twiggy_wasm_api.wasm --out-dir .

This should produce two artifacts in the current directory:

  1. twiggy_wasm_api_bg.wasm: The WebAssembly file containing twiggy.
  2. twiggy_wasm_api.js: The JavaScript bindings to twiggy's WebAssembly.

You can now use twiggy from JavaScript like this:

import { Items, Monos } from './twiggy_wasm_api';

// Parse a binary's data into a collection of items.
const items = Items.parse(myData);

// Configure an analysis and its options.
const opts =;

// Run the analysis on the parsed items.
const monos = JSON.parse(items.monos(opts));

๐Ÿ”Ž Supported Binary Formats

Full Support

twiggy currently supports these binary formats:

  • โœ”๏ธ WebAssembly's .wasm format

Partial, Work-in-Progress Support

twiggy has partial, work-in-progress support for these binary formats when they have DWARF debug info:

  • โš  ELF
  • โš  Mach-O


  • โŒ PE/COFF

Although twiggy doesn't currently support these binary formats, it is designed with extensibility in mind. The input is translated into a format-agnostic internal representation (IR), and adding support for new formats only requires parsing them into this IR. The vast majority of twiggy will not need modification.

We would love to gain support for new binary formats! If you're interested in helping out with that implementation work, read this to learn how to contribute to Twiggy!

๐Ÿ™Œ Contributing to Twiggy

Hi! We'd love to have your contributions! If you want help or mentorship, reach out to us in a GitHub issue, or ping fitzgen in #rust-wasm on and introduce yourself.

Code of Conduct

We abide by the Rust Code of Conduct and ask that you do as well.


Building for the Native Target

$ cargo build --all --exclude twiggy-wasm-api

Building for the wasm32-unknown-unknown Target

$ JOB=wasm ./ci/


$ cargo test --all --exclude twiggy-wasm-api

Authoring New Tests

Integration tests live in the twiggy/tests directory:

โ”œโ”€โ”€ expectations
โ”œโ”€โ”€ fixtures
  • The twiggy/tests/ file contains the #[test] definitions.

  • The twiggy/tests/fixtures directory contains input binaries for tests.

  • The twiggy/tests/expectations directory contains the expected output of test commands.

Updating Test Expectations

To automatically update all test expectations, you can run the tests with the TWIGGY_UPDATE_TEST_EXPECTATIONS=1 environment variable set. Make sure that you look at the changes before committing them, and that they match your intentions!

TIP: You can use git add -p to examine individual hunks when staging changes before committing!

Code Formatting

We use rustfmt to enforce a consistent code style across the whole code base.

You can install the latest version of rustfmt with this command:

$ rustup update
$ rustup component add rustfmt --toolchain stable

Ensure that ~/.rustup/toolchains/$YOUR_HOST_TARGET/bin/ is on your $PATH.

Once that is taken care of, you can (re)format all code by running this command from the root of the repository:

$ cargo fmt --all


We use clippy to lint the codebase. This helps avoid common mistakes, and ensures that code is correct, performant, and idiomatic.

You can install the latest version of clippy with this command:

$ rustup update
$ rustup component add clippy --toolchain stable

Once that is complete, you can lint your code to check for mistakes by running this command from the root of the repository:

$ cargo clippy

Pull Requests

All pull requests must be reviewed and approved of by at least one team member before merging. See Contributions We Want for details on what should be included in what kind of pull request.

Contributions We Want

  • Bug fixes! Include a regression test.

  • Support for more binary formats! See this issue for details.

  • New analyses and queries! Help expose information about monomorphizations or inlining. Report diffs between two versions of the same binary. Etc...

If you make two of these kinds of contributions, you should seriously consider joining our team!

Where We Need Help

  • Issues labeled "help wanted" are issues where we could use a little help from you.

  • Issues labeled "mentored" are issues that don't really involve any more investigation, just implementation. We've outlined what needs to be done, and a team member has volunteered to help whoever claims the issue to implement it, and get the implementation merged.

  • Issues labeled "good first issue" are issues where fixing them would be a great introduction to the code base.





Team members review pull requests, triage new issues, mentor new contributors, and maintain the Twiggy project.

Larger, more nuanced decisions about design, architecture, breaking changes, trade offs, etc are made by team consensus. In other words, decisions on things that aren't straightforward improvements or bug fixes to things that already exist in twiggy. If consensus can't be made, then fitzgen has the last word.

We need more team members! Drop a comment on this issue if you are interested in joining.