diff --git a/Cargo.toml b/Cargo.toml
index b004b39b..5154106a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -48,6 +48,7 @@ wasm-bindgen-test-crate-b = { path = 'tests/crates/b', version = '0.1' }
 
 [workspace]
 members = [
+  "benchmarks",
   "crates/cli",
   "crates/js-sys",
   "crates/macro/ui-tests",
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 7dc20ca2..dbd4c449 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -144,15 +144,9 @@ jobs:
     steps:
       - template: ci/azure-install-rust.yml
       - template: ci/azure-install-sccache.yml
+      - template: ci/azure-install-wasm-pack.yml
       - script: mv _package.json package.json && npm install && rm package.json
         displayName: "run npm install"
-      - script: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
-        displayName: "install wasm-pack"
-      - script: |
-          set -ex
-          cargo build -p wasm-bindgen-cli
-          ln -snf `pwd`/target/debug/wasm-bindgen $HOME/.cargo/bin/wasm-bindgen
-        displayName: "install wasm-bindgen for `wasm-pack` to use"
       - script: |
           for dir in `ls examples | grep -v README | grep -v asm.js | grep -v raytrace | grep -v without-a-bundler`; do
             (cd examples/$dir &&
@@ -190,6 +184,19 @@ jobs:
           artifactName: examples2
           targetPath: '$(Build.ArtifactStagingDirectory)'
 
+  - job: build_benchmarks
+    displayName: "Build benchmarks"
+    steps:
+      - template: ci/azure-install-rust.yml
+      - template: ci/azure-install-sccache.yml
+      - template: ci/azure-install-wasm-pack.yml
+      - script: wasm-pack build --target web benchmarks
+        displayName: "build benchmarks"
+      - task: PublishPipelineArtifact@0
+        inputs:
+          artifactName: benchmarks
+          targetPath: benchmarks
+
   - job: dist_linux
     displayName: "Dist Linux binary"
     steps:
@@ -292,6 +299,7 @@ jobs:
       - dist_windows
       - build_examples
       - build_raytrace
+      - build_benchmarks
     displayName: "Deploy everything"
     steps:
       - template: ci/azure-install-rust.yml
@@ -315,6 +323,11 @@ jobs:
         inputs:
           artifactName: examples2
           targetPath: gh-pages/exbuild/raytrace-parallel
+      - task: DownloadPipelineArtifact@0
+        displayName: "Download benchmarks"
+        inputs:
+          artifactName: benchmarks
+          targetPath: gh-pages/benchmarks
       - task: DownloadPipelineArtifact@0
         displayName: "Download dist - windows"
         inputs:
diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml
new file mode 100644
index 00000000..7ffef776
--- /dev/null
+++ b/benchmarks/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "wasm-bindgen-benchmark"
+version = "0.1.0"
+authors = ["The wasm-bindgen Developers"]
+
+[dependencies]
+wasm-bindgen = "0.2.43"
+web-sys = { version = "0.3.20", features = ['Node'] }
+
+[lib]
+crate-type = ['cdylib']
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 00000000..381c7913
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,54 @@
+# Microbenchmarks for `wasm-bindgen`
+
+This folder houses a number of microbenchmarks for `wasm-bindgen`. These, like
+all microbenchmarks, should be taken with a grain of salt. They are intended to
+help developers understand changes over time, but they are not intended to be a
+performance suite for WebAssembly for Rust.
+
+[View benchmarks for `master` branch online][online]
+
+[online]: https://rustwasm.github.io/wasm-bindgen/benchmarks/
+
+## Building and Running
+
+First, copy the benchmarks to a temporary directory:
+
+```
+$ cp ./benchmarks /some/other/directory
+```
+
+Next, `cd` into that directory and execute:
+
+```
+$ wasm-pack build --target web
+```
+
+Next, use your favorite static file server to host the current directory. For
+example using the [`https` crate](https://crates.io/crates/https):
+
+```
+$ http
+```
+
+Then open up a web browser and view http://localhost:8000, for example.
+
+You should be presented a page with lots of `(run)` links, where when you click
+them it will execute the benchmark and then display the result.
+
+## Benchmark Architecture
+
+Currently benchmarks are pretty bare bones. They just use benchmark.js to
+generate statistics which are then rendered to the screen. Benchmarks are listed
+one-by-one in `index.html` where a `td` exists for each benchmark. In `index.js`
+each of the `td`'s `id` properties are hooked up to an actual function to
+benchmark, depending on what's being benchmarked.
+
+Relevant files are:
+
+* `index.html` - the page showing all benchmarks
+* `index.js` - the driver JS for all benchmarks
+* `globals.js` - global JS functions imported by all other benchmarks
+* `js-bencharks.js` - the JS functions that we're benchmarking
+* `src/lib.rs` - the Rust/`wasm-bindgen` functions we're benchmarking
+* `raw.wast`/`raw.wasm` - a raw handwritten WebAssembly file used in some
+  benchmarks. A compiled version of this is checked into the repository.
diff --git a/benchmarks/globals.js b/benchmarks/globals.js
new file mode 100644
index 00000000..0412158e
--- /dev/null
+++ b/benchmarks/globals.js
@@ -0,0 +1,5 @@
+export function jsthunk() {}
+export function add(a, b) { return a + b; }
+export class Foo {
+  bar() {}
+}
diff --git a/benchmarks/index.html b/benchmarks/index.html
new file mode 100644
index 00000000..83a99337
--- /dev/null
+++ b/benchmarks/index.html
@@ -0,0 +1,295 @@
+<html>
+  <head>
+    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
+    <style>
+
+body {
+  max-width: 1000px;
+  margin: 0 auto;
+}
+thead td {
+  font-weight: bold;
+}
+table td {
+  border: 1px solid black;
+  padding: 10px;
+}
+
+.about {
+  display: none;
+}
+    </style>
+    <script src='index.js' type=module async></script>
+  </head>
+  <body>
+    <a href='https://github.com/alexcrichton/rust-wasm-benchmark'>Source code</a>
+
+    <h1>JS / wasm-bindgen comparison</h1>
+
+    <p>
+      These benchmarks are meant to compare WebAssembly costs using raw wasm
+      files and wasm-bindgen itself against the JS equivalents. These
+      microbenchmarks aren't really representative of WebAssembly performance,
+      but can be useful data points about how expensive it is to cross
+      boundaries for example.
+    </p>
+    <p>
+      For all benchmarks higher numbers are better numbers.
+    </p>
+
+    <table id='js-vs-wasm-bindgen'>
+      <thead>
+        <tr>
+          <td>Benchmark</td>
+          <td>wasm-bindgen</td>
+          <td>JS</td>
+          <td><code>*.wast</code></td>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>
+            Call a thunk
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmarks tests out how long it take JS to call a thunk in
+              the given language. For example JS will call a JS thunk or
+              JS will call a wasm function that does nothing.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_thunk'></td>
+          <td class='bm' id='js_thunk'></td>
+          <td class='bm' id='raw_thunk'></td>
+        </tr>
+        <tr>
+          <td>
+            Call an adder
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmarks tests out how long it take JS to call a function
+              in the target language which adds two numbers and returns the
+              result. This is likely to be similar to the previous benchmark
+              in terms of results, but it's thought that some extra computation
+              may show overheads differently.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_thunk'></td>
+          <td class='bm' id='js_thunk'></td>
+          <td class='bm' id='raw_thunk'></td>
+        </tr>
+
+          <td>
+            Call a JS thunk
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmarks tests out how long it takes to call a JS function
+              10,000 times. Each language/framework has a function which takes a
+              parameter of how many times to call an imported JS function.
+              Remember that JS itself benefits from inlining, where as JS cannot
+              be inlined into WebAssembly. In these cases the imported function
+              performs no work, it just returns immediately.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_js_thunk_n_times'></td>
+          <td class='bm' id='js_call_js_thunk_n_times'></td>
+          <td class='bm' id='raw_call_js_thunk_n_times'></td>
+        </tr>
+
+        <tr>
+          <td>
+            Call a JS function which adds
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmark is similar to the previous thunk benchmark except
+              that this time the imported function will add its two arguments
+              and returns the result. This helps measure the overhead of
+              sending data like integers back and forth.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_js_add_n_times'></td>
+          <td class='bm' id='js_call_js_add_n_times'></td>
+          <td class='bm' id='raw_call_js_add_n_times'></td>
+        </tr>
+
+        <tr>
+          <td>
+            Calculate Fib(40)
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmarks calculates the 40th fibonacci number. It in
+              theory should favor wasm since wasm is "better a compute", but
+              a good JIT will probably make the code roughly equivalent.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_fib_40'></td>
+          <td class='bm' id='js_fib_40'></td>
+        </tr>
+
+        <tr>
+          <td>
+            Access <code>Node.firstChild</code>
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmark attempts to see the cost of accessing a DOM
+              property from both JS and WebAssembly. We access the DOM property
+              as fast as possible in WebAssembly and otherwise just access it
+              as a normal property in JS.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_node_first_child_n_times'></td>
+          <td class='bm' id='js_call_node_first_child_n_times'></td>
+        </tr>
+
+        <tr>
+          <td>
+            Access <code>Node.nodeType</code>
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmark attempts to see the cost of accessing a DOM
+              property from both JS and WebAssembly. We access the DOM property
+              as fast as possible in WebAssembly and otherwise just access it
+              as a normal property in JS.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_node_node_type_n_times'></td>
+          <td class='bm' id='js_call_node_node_type_n_times'></td>
+        </tr>
+
+        <tr>
+          <td>
+            Count types of nodes on a page
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This is intended to be a "flavorful DOM benchmark" which
+              exercises DOM functionality from WebAssembly, specifically
+              counting the number of types of each node on a page.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_count_node_types'></td>
+          <td class='bm' id='js_count_node_types'></td>
+        </tr>
+      </tbody>
+    </table>
+
+    <h1>wasm-bindgen benchmarks</h1>
+
+    <p>
+      These benchmarks don't compare against JS but are instead more intended
+      to be compared against each other, across browsers, or across versions of
+      wasm-bindgen.
+    </p>
+
+    <table id='js-vs-wasm-bindgen'>
+      <thead>
+        <tr>
+          <td>Benchmark</td>
+          <td>Result</td>
+        </tr>
+      </thead>
+      <tbody id='wbindgen-body'>
+        <tr>
+          <td>
+            Access <code>Node.nodeType</code> with <code>final</code>
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This is similar to the <code>Node.nodeType</code> benchmark above
+              except that it uses the <code>final</code> attribute in
+              wasm-bindgen.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_first_child_final_n_times'></td>
+        </tr>
+        <tr>
+          <td>
+            Access <code>Node.nodeType</code> with <code>structural</code>
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This is similar to the <code>Node.nodeType</code> benchmark above
+              except that it uses the <code>structural</code> attribute in
+              wasm-bindgen.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_first_child_structural_n_times'></td>
+        </tr>
+
+        <tr>
+          <td>
+            Call a custom JS class <code>Foo.bar</code> method with
+            <code>final</code>
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This is similar to the <code>Node.nodeType</code> benchmark above
+              except that it's not calling a DOM method but rather a custom JS
+              class's method.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_foo_bar_final_n_times'></td>
+        </tr>
+        <tr>
+          <td>
+            Call a custom JS class <code>Foo.bar</code> method with
+            <code>structural</code>
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This is similar to the <code>Node.nodeType</code> benchmark above
+              except that it's not calling a DOM method but rather a custom JS
+              class's method.
+            </p>
+          </td>
+
+          <td class='bm' id='wbindgen_call_foo_bar_structural_n_times'></td>
+        </tr>
+
+        <tr style='display:none' class='str-benchmark'>
+          <td>
+            Pass <span class='str'></span> to/from wasm-bindgen
+
+            <a class='about-open' href='#'>(?)</a>
+
+            <p class='about'>
+              This benchmarks the overhead of passing strings to wasm and
+              also receiving them from wasm.
+            </p>
+          </td>
+
+          <td class='bm'></td>
+        </tr>
+      </tbody>
+    </table>
+  </body>
+</html>
diff --git a/benchmarks/index.js b/benchmarks/index.js
new file mode 100644
index 00000000..ac603b63
--- /dev/null
+++ b/benchmarks/index.js
@@ -0,0 +1,182 @@
+// Benchmarking framework that we're using
+import 'https://unpkg.com/lodash@4.17.11/lodash.js';
+import 'https://unpkg.com/benchmark@2.1.4/benchmark.js';
+
+// Import lots of functions from JS/wasm, and rename everything to have the
+// namespace of where it's coming from.
+import wbindgen_init, {
+  call_js_thunk_n_times as wbindgen_call_js_thunk_n_times,
+  call_js_add_n_times as wbindgen_call_js_add_n_times,
+  thunk as wbindgen_thunk,
+  add as wbindgen_add,
+  fibonacci as wbindgen_fibonacci,
+  call_node_first_child_n_times as wbindgen_call_node_first_child_n_times,
+  call_node_node_type_n_times as wbindgen_call_node_node_type_n_times,
+  count_node_types as wbindgen_count_node_types,
+  call_first_child_final_n_times as wbindgen_call_first_child_final_n_times,
+  call_first_child_structural_n_times as wbindgen_call_first_child_structural_n_times,
+  call_foo_bar_final_n_times as wbindgen_call_foo_bar_final_n_times,
+  call_foo_bar_structural_n_times as wbindgen_call_foo_bar_structural_n_times,
+  str_roundtrip as wbindgen_str_roundtrip,
+} from './pkg/wasm_bindgen_benchmark.js';
+import {
+  call_js_thunk_n_times as js_call_js_thunk_n_times,
+  call_js_add_n_times as js_call_js_add_n_times,
+  thunk as js_thunk,
+  add as js_add,
+  fibonacci as js_fibonacci,
+  call_node_first_child_n_times as js_call_node_first_child_n_times,
+  call_node_node_type_n_times as js_call_node_node_type_n_times,
+  count_node_types as js_count_node_types,
+} from './js-benchmarks.js';
+import * as globals from './globals.js';
+import { Lock } from './utils.js';
+
+// These are set for `raw.wasm`, which we import and configure manually:
+let raw_call_js_thunk_n_times = null;
+let raw_call_js_add_n_times = null;
+let raw_thunk = null;
+let raw_add = null;
+
+// Create a `Map` of all benchmarks that we're going to execute, where the map
+// is from a benchmark's name to a thunk to execute for the benchmark.
+function makeBenchmarks() {
+  const benchmarks = new Map();
+
+  benchmarks.wbindgen_thunk = wbindgen_thunk;
+  benchmarks.raw_thunk = raw_thunk;
+  benchmarks.js_thunk = js_thunk;
+
+  benchmarks.wbindgen_fib_40 = () => wbindgen_fibonacci(40);
+  benchmarks.js_fib_40 = () => js_fibonacci(40);
+
+  benchmarks.wbindgen_add = () => wbindgen_add(2, 3);
+  benchmarks.raw_add = () => raw_add(2, 3);
+  benchmarks.js_add = () => js_add(2, 3);
+
+  benchmarks.js_call_js_thunk_n_times = () => js_call_js_thunk_n_times(10000);
+  benchmarks.raw_call_js_thunk_n_times = () => raw_call_js_thunk_n_times(10000);
+  benchmarks.wbindgen_call_js_thunk_n_times = () => wbindgen_call_js_thunk_n_times(10000);
+
+  benchmarks.js_call_js_add_n_times = () => js_call_js_add_n_times(10000, 2, 3);
+  benchmarks.raw_call_js_add_n_times = () => raw_call_js_add_n_times(10000, 2, 3);
+  benchmarks.wbindgen_call_js_add_n_times = () => wbindgen_call_js_add_n_times(10000, 2, 3);
+
+  const list = [];
+  for (let i = 0; i < 10; i++)
+    list.push(document.body);
+  benchmarks.wbindgen_call_node_first_child_n_times = () => wbindgen_call_node_first_child_n_times(1000, list);
+  benchmarks.js_call_node_first_child_n_times = () => js_call_node_first_child_n_times(1000, list);
+  benchmarks.wbindgen_call_node_node_type_n_times = () => wbindgen_call_node_node_type_n_times(1000, list);
+  benchmarks.js_call_node_node_type_n_times = () => js_call_node_node_type_n_times(1000, list);
+
+  const body = document.body;
+  benchmarks.wbindgen_count_node_types = () => wbindgen_count_node_types(body);
+  benchmarks.js_count_node_types = () => js_count_node_types(body);
+
+  benchmarks.wbindgen_call_first_child_final_n_times = () => wbindgen_call_first_child_final_n_times(10000, body);
+  benchmarks.wbindgen_call_first_child_structural_n_times = () => wbindgen_call_first_child_structural_n_times(10000, body);
+
+  const foo = new globals.Foo();
+  benchmarks.wbindgen_call_foo_bar_final_n_times = () => wbindgen_call_foo_bar_final_n_times(10000, foo);
+  benchmarks.wbindgen_call_foo_bar_structural_n_times = () => wbindgen_call_foo_bar_structural_n_times(10000, foo);
+
+
+  const strings = {
+    ascii_small: 'ja',
+    ascii_medium: 'aym0566x',
+    ascii_number: '505874924095815681',
+    ascii_date: 'Sun Aug 31 00:29:15 +0000 2014',
+    ascii_url: 'https://pbs.twimg.com/profile_images/497760886795153410/LDjAwR_y_normal.jpeg',
+    ascii_link: '<a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>',
+    unicode: '@aym0566x \n\n名前:前田あゆみ\n第一印象:なんか怖っ!\n今の印象:とりあえずキモい。噛み合わない\n好きなところ:ぶすでキモいとこ😋✨✨\n思い出:んーーー、ありすぎ😊❤️\nLINE交換できる?:あぁ……ごめん✋\nトプ画をみて:照れますがな😘✨\n一言:お前は一生もんのダチ💖'
+  }
+  const template = document.querySelector('tr.str-benchmark');
+  template.remove();
+  const tbody = document.querySelector('tbody#wbindgen-body');
+  for (const bm in strings) {
+    const s = strings[bm];
+    const bm_name = `wbindgen_str_${bm}`;
+    benchmarks[bm_name] = () => wbindgen_str_roundtrip(s);
+
+    const row = template.cloneNode(true);
+    row.querySelector('.str').textContent = bm;
+    row.querySelector('td.bm').id = bm_name;
+    row.removeAttribute('style');
+    tbody.appendChild(row);
+  }
+
+  return benchmarks;
+}
+
+// Set up the page and initialize all event handlers and such.
+function run() {
+  const benchmarks = makeBenchmarks();
+
+  const benchmarkLock = new Lock();
+  for (const td of document.querySelectorAll('td.bm')) {
+    const bm = benchmarks[td.id];
+    if (typeof bm !== 'function')
+      throw new Error(`no benchmark registered for ${td.id}`);
+
+    const run = document.createElement('a');
+    run.href = '#';
+    run.innerText = '(run)';
+    run.onclick = function() {
+      benchmarkLock.withLock(async () => {
+        await executeAndUpdate(td.id, bm, td);
+      });
+      run.remove();
+      td.innerText = 'executing ...';
+      return false;
+    };
+    td.appendChild(run);
+  }
+
+  for (const a of document.querySelectorAll('.about-open')) {
+    a.onclick = function() {
+      a.nextElementSibling.style.display = 'block';
+      a.remove();
+      return false;
+    };
+  }
+}
+
+async function executeAndUpdate(name, bm, td) {
+  const result = await executeBenchmark(name, bm);
+  console.log(result.target);
+  const rme = Math.round(result.target.stats.rme * 100) / 100;
+  td.innerText = `${Math.round(result.target.hz).toLocaleString()}/s ±${rme}%`;
+}
+
+function executeBenchmark(name, bm) {
+  return new Promise((resolve, reject) => {
+    const suite = new Benchmark.Suite();
+    suite.add(name, bm);
+    suite.on('cycle', resolve);
+    suite.run({ async: true });
+  });
+}
+
+// Load wasm files and when they're done (plus the DOM) then we initialize
+// everything
+const wasms = [];
+wasms.push(wbindgen_init('./pkg/wasm_bindgen_benchmark_bg.wasm'));
+wasms.push(fetch('./raw.wasm')
+  .then(r => r.arrayBuffer())
+  .then(m => WebAssembly.instantiate(m, { './globals.js': globals }))
+  .then(m => {
+    raw_call_js_thunk_n_times = m.instance.exports.call_js_thunk_n_times;
+    raw_call_js_add_n_times = m.instance.exports.call_js_add_n_times;
+    raw_thunk = m.instance.exports.thunk;
+    raw_add = m.instance.exports.add;
+  }));
+
+Promise.all(wasms)
+  .then(() => {
+    if (document.readyState === 'loading')
+      document.addEventListener('DOMContentLoaded', run);
+    else
+      run();
+  })
+  .catch(console.error);
diff --git a/benchmarks/js-benchmarks.js b/benchmarks/js-benchmarks.js
new file mode 100644
index 00000000..d6b56614
--- /dev/null
+++ b/benchmarks/js-benchmarks.js
@@ -0,0 +1,76 @@
+import { jsthunk, add as jsadd } from './globals.js';
+
+export function fibonacci(n) {
+  let a = 1;
+  let b = 1;
+  let tmp = 0;
+
+  while (n > 1) {
+    tmp = b;
+    b += a;
+    a = tmp;
+    --n;
+  }
+
+  return a;
+}
+
+export function thunk() {}
+
+export function call_js_thunk_n_times(n) {
+  for (var i = 0; i < n; i++) {
+    jsthunk();
+  }
+}
+
+export function add(a, b) {
+  return a + b;
+}
+
+export function call_js_add_n_times(n, a, b) {
+  for (var i = 0; i < n; i++) {
+    jsadd(a, b);
+  }
+}
+
+export function call_node_first_child_n_times(n, array_of_elements) {
+  for (let i = 0; i < n; i++) {
+    for (const element of array_of_elements)
+      if (element.firstChild === null)
+        throw new Error("bad");
+  }
+}
+
+export function call_node_node_type_n_times(n, array_of_elements) {
+  for (let i = 0; i < n; i++) {
+    for (const element of array_of_elements)
+      if (element.nodeType === 100)
+        throw new Error("bad");
+  }
+}
+
+export function call_node_has_child_nodes_n_times(n, array_of_elements) {
+  for (let i = 0; i < n; i++) {
+    for (const element of array_of_elements)
+      if (!element.hasChildNodes())
+        throw new Error("bad");
+  }
+}
+
+export function count_node_types(element) {
+  const types = [];
+
+  function count(node, types) {
+    while(node) {
+      const type = node.nodeType;
+      while (types.length <= type)
+        types.push(0);
+      types[type] += 1;
+      count(node.firstChild, types);
+      node = node.nextSibling;
+    }
+  }
+
+  count(element, types);
+  return types;
+}
diff --git a/benchmarks/raw.wasm b/benchmarks/raw.wasm
new file mode 100644
index 00000000..00580e29
Binary files /dev/null and b/benchmarks/raw.wasm differ
diff --git a/benchmarks/raw.wast b/benchmarks/raw.wast
new file mode 100644
index 00000000..97298226
--- /dev/null
+++ b/benchmarks/raw.wast
@@ -0,0 +1,52 @@
+(module
+  (import "./globals.js" "jsthunk" (func $js_thunk))
+  (import "./globals.js" "add" (func $js_add (param i32) (param i32) (result i32)))
+
+  (export "call_js_thunk_n_times" (func $call_thunk))
+  (export "call_js_add_n_times" (func $call_add))
+  (export "thunk" (func $thunk))
+  (export "add" (func $add))
+
+  (func $call_thunk (param i32)
+    block
+      get_local 0
+      i32.eqz
+      br_if 0
+      loop
+        call $js_thunk
+        get_local 0
+        i32.const 1
+        i32.sub
+        tee_local 0
+        br_if 0
+      end
+    end
+  )
+
+  (func $call_add (param i32) (param i32) (param i32)
+    block
+      get_local 0
+      i32.eqz
+      br_if 0
+      loop
+        get_local 2
+        get_local 1
+        call $js_add
+        drop
+        get_local 0
+        i32.const 1
+        i32.sub
+        tee_local 0
+        br_if 0
+      end
+    end
+  )
+
+  (func $thunk)
+
+  (func $add (param i32) (param i32) (result i32)
+    get_local 0
+    get_local 1
+    i32.add
+  )
+)
diff --git a/benchmarks/src/lib.rs b/benchmarks/src/lib.rs
new file mode 100644
index 00000000..2b8e1736
--- /dev/null
+++ b/benchmarks/src/lib.rs
@@ -0,0 +1,179 @@
+extern crate wasm_bindgen;
+extern crate web_sys;
+
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsCast;
+use web_sys::Node;
+
+#[wasm_bindgen(raw_module = "../globals.js")]
+extern {
+    #[wasm_bindgen(js_name = jsthunk)]
+    fn js_thunk();
+    #[wasm_bindgen(js_name = add)]
+    fn js_add(a: i32, b: i32) -> i32;
+
+    pub type Foo;
+    #[wasm_bindgen(method, final, js_name = bar)]
+    fn bar_final(this: &Foo);
+    #[wasm_bindgen(method, structural, js_name = bar)]
+    fn bar_structural(this: &Foo);
+
+    #[wasm_bindgen(js_name = jsthunk)]
+    fn doesnt_throw();
+    #[wasm_bindgen(catch, js_name = jsthunk)]
+    fn doesnt_throw_catch() -> Result<(), JsValue>;
+}
+
+#[wasm_bindgen]
+pub fn call_js_thunk_n_times(n: usize) {
+    for _ in 0..n {
+        js_thunk();
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_js_add_n_times(n: usize, a: i32, b: i32) {
+    for _ in 0..n {
+        js_add(a, b);
+    }
+}
+
+#[wasm_bindgen]
+pub fn thunk() {}
+
+#[wasm_bindgen]
+pub fn add(a: i32, b: i32) -> i32 {
+    a + b
+}
+
+static mut FIB_HIGH: i32 = 0;
+
+#[wasm_bindgen]
+pub fn fibonacci(n: i32) -> i32 {
+    let mut a = 1u64;
+    let mut b = 1;
+    for _ in 0..n {
+        let tmp = b;
+        b += a;
+        a = tmp;
+    }
+    unsafe { FIB_HIGH = (a >> 32) as i32; }
+    return a as i32
+}
+
+#[wasm_bindgen]
+pub fn fibonacci_high() -> i32 {
+    unsafe { FIB_HIGH }
+}
+
+#[wasm_bindgen]
+pub fn call_foo_bar_final_n_times(n: usize, foo: &Foo) {
+    for _ in 0..n {
+        foo.bar_final();
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_foo_bar_structural_n_times(n: usize, foo: &Foo) {
+    for _ in 0..n {
+        foo.bar_structural();
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_doesnt_throw_n_times(n: usize) {
+    for _ in 0..n {
+        doesnt_throw();
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_doesnt_throw_with_catch_n_times(n: usize) {
+    for _ in 0..n {
+        if let Err(e) = doesnt_throw_catch() {
+            wasm_bindgen::throw_val(e);
+        }
+    }
+}
+
+#[wasm_bindgen]
+extern {
+    pub type Element;
+
+    #[wasm_bindgen(method, js_name = firstChild, final, getter)]
+    fn first_child_final(this: &Element) -> Element;
+    #[wasm_bindgen(method, js_name = firstChild, structural, getter)]
+    fn first_child_structural(this: &Element) -> Element;
+}
+
+#[wasm_bindgen]
+pub fn call_first_child_final_n_times(n: usize, element: &Element) {
+    for _ in 0..n {
+        drop(element.first_child_final());
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_first_child_structural_n_times(n: usize, element: &Element) {
+    for _ in 0..n {
+        drop(element.first_child_structural());
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_node_first_child_n_times(n: usize, elements: Vec<JsValue>) {
+    for _ in 0..n {
+        for element in elements.iter() {
+            let element = element.unchecked_ref::<Node>();
+            assert!(element.first_child().is_some());
+        }
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_node_node_type_n_times(n: usize, elements: Vec<JsValue>) {
+    for _ in 0..n {
+        for element in elements.iter() {
+            let element = element.unchecked_ref::<Node>();
+            assert!(element.node_type() != 100);
+        }
+    }
+}
+
+#[wasm_bindgen]
+pub fn call_node_has_child_nodes_n_times(n: usize, elements: Vec<JsValue>) {
+    for _ in 0..n {
+        for element in elements.iter() {
+            let element = element.unchecked_ref::<Node>();
+            assert!(element.has_child_nodes());
+        }
+    }
+}
+
+#[wasm_bindgen]
+pub fn count_node_types(element: Node) {
+    let mut count = Vec::new();
+    count_node_types(element, &mut count);
+
+    fn count_node_types(mut element: Node, count: &mut Vec<u32>) {
+        loop {
+            let t = element.node_type();
+            if t as usize >= count.len() {
+                count.resize(t as usize + 1, 0);
+            }
+            count[t as usize] += 1;
+            if let Some(s) = element.first_child() {
+                count_node_types(s, count);
+            }
+            match element.next_sibling() {
+                Some(s) => element = s,
+                None => break,
+            }
+        }
+    }
+}
+
+#[wasm_bindgen]
+pub fn str_roundtrip(s: String) -> String {
+    s
+}
diff --git a/benchmarks/utils.js b/benchmarks/utils.js
new file mode 100644
index 00000000..d2b4af24
--- /dev/null
+++ b/benchmarks/utils.js
@@ -0,0 +1,14 @@
+export class Lock {
+  constructor() {
+    this.lockHolder = null;
+  }
+
+  async withLock(scope) {
+    while (this.lockHolder !== null) {
+      await this.lockHolder;
+    }
+    this.lockHolder = Promise.resolve(null).then(scope);
+    await this.lockHolder;
+    this.lockHolder = null;
+  }
+}
diff --git a/ci/azure-install-wasm-pack.yml b/ci/azure-install-wasm-pack.yml
new file mode 100644
index 00000000..4759d82e
--- /dev/null
+++ b/ci/azure-install-wasm-pack.yml
@@ -0,0 +1,8 @@
+steps:
+  - script: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
+    displayName: "install wasm-pack"
+  - script: |
+      set -ex
+      cargo build -p wasm-bindgen-cli
+      ln -snf `pwd`/target/debug/wasm-bindgen $HOME/.cargo/bin/wasm-bindgen
+    displayName: "install wasm-bindgen for `wasm-pack` to use"