diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index 15433041..c02bd9b1 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -46,6 +46,9 @@ pub struct Export {
     pub comments: Vec<String>,
     /// The name of the rust function/method on the rust side.
     pub rust_name: Ident,
+    /// Whether or not this function should be flagged as the wasm start
+    /// function.
+    pub start: bool,
 }
 
 /// The 3 types variations of `self`.
diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs
index 507cb974..3fb57485 100644
--- a/crates/backend/src/codegen.rs
+++ b/crates/backend/src/codegen.rs
@@ -450,12 +450,21 @@ impl TryToTokens for ast::Export {
         let argtys = self.function.arguments.iter().map(|arg| &arg.ty);
         let attrs = &self.function.rust_attrs;
 
+        let start_check = if self.start {
+            quote! {
+                const _ASSERT: fn() = || #ret_ty { loop {} };
+            }
+        } else {
+            quote! {}
+        };
+
         (quote! {
             #(#attrs)*
             #[export_name = #export_name]
             #[allow(non_snake_case)]
             #[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
             pub extern "C" fn #generated_name(#(#args),*) #ret_ty {
+                #start_check
                 // Scope all local variables to be destroyed after we call the
                 // function to ensure that `#convert_ret`, if it panics, doesn't
                 // leak anything.
diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs
index 0f8512f5..adaec24d 100644
--- a/crates/backend/src/encode.rs
+++ b/crates/backend/src/encode.rs
@@ -82,6 +82,7 @@ fn shared_export<'a>(export: &'a ast::Export, intern: &'a Interner) -> Export<'a
         is_constructor: export.is_constructor,
         function: shared_function(&export.function, intern),
         comments: export.comments.iter().map(|s| &**s).collect(),
+        start: export.start,
     }
 }
 
diff --git a/crates/cli-support/src/js/closures.rs b/crates/cli-support/src/js/closures.rs
index 377bd5aa..a3193de1 100644
--- a/crates/cli-support/src/js/closures.rs
+++ b/crates/cli-support/src/js/closures.rs
@@ -18,6 +18,7 @@ use parity_wasm::elements::*;
 use descriptor::Descriptor;
 use js::js2rust::Js2Rust;
 use js::Context;
+use wasm_utils::Remap;
 
 pub fn rewrite(input: &mut Context) -> Result<(), Error> {
     let info = ClosureDescriptors::new(input);
@@ -37,15 +38,21 @@ pub fn rewrite(input: &mut Context) -> Result<(), Error> {
     // function indices. We're going to be injecting a few imported functions
     // below which will shift the index space for all defined functions.
     input.parse_wasm_names();
-    Remap {
-        code_idx_to_descriptor: &info.code_idx_to_descriptor,
-        old_num_imports: input
-            .module
-            .import_section()
-            .map(|s| s.functions())
-            .unwrap_or(0) as u32,
-    }
-    .remap_module(input.module);
+    let old_num_imports = input
+        .module
+        .import_section()
+        .map(|s| s.functions())
+        .unwrap_or(0) as u32;
+    Remap(|idx| {
+        // If this was an imported function we didn't reorder those, so nothing
+        // to do.
+        if idx < old_num_imports {
+            return idx
+        }
+        // ... otherwise we're injecting a number of new imports, so offset
+        // everything.
+        idx + info.code_idx_to_descriptor.len() as u32
+    }).remap_module(input.module);
 
     info.delete_function_table_entries(input);
     info.inject_imports(input)?;
@@ -298,119 +305,3 @@ impl ClosureDescriptors {
         }
     }
 }
-
-struct Remap<'a> {
-    code_idx_to_descriptor: &'a BTreeMap<u32, DescribeInstruction>,
-    old_num_imports: u32,
-}
-
-impl<'a> Remap<'a> {
-    fn remap_module(&self, module: &mut Module) {
-        for section in module.sections_mut() {
-            match section {
-                Section::Export(e) => self.remap_export_section(e),
-                Section::Element(e) => self.remap_element_section(e),
-                Section::Code(e) => self.remap_code_section(e),
-                Section::Start(i) => {
-                    self.remap_idx(i);
-                }
-                Section::Name(n) => self.remap_name_section(n),
-                _ => {}
-            }
-        }
-    }
-
-    fn remap_export_section(&self, section: &mut ExportSection) {
-        for entry in section.entries_mut() {
-            self.remap_export_entry(entry);
-        }
-    }
-
-    fn remap_export_entry(&self, entry: &mut ExportEntry) {
-        match entry.internal_mut() {
-            Internal::Function(i) => {
-                self.remap_idx(i);
-            }
-            _ => {}
-        }
-    }
-
-    fn remap_element_section(&self, section: &mut ElementSection) {
-        for entry in section.entries_mut() {
-            self.remap_element_entry(entry);
-        }
-    }
-
-    fn remap_element_entry(&self, entry: &mut ElementSegment) {
-        for member in entry.members_mut() {
-            self.remap_idx(member);
-        }
-    }
-
-    fn remap_code_section(&self, section: &mut CodeSection) {
-        for body in section.bodies_mut() {
-            self.remap_func_body(body);
-        }
-    }
-
-    fn remap_func_body(&self, body: &mut FuncBody) {
-        self.remap_instructions(body.code_mut());
-    }
-
-    fn remap_instructions(&self, code: &mut Instructions) {
-        for instr in code.elements_mut() {
-            self.remap_instruction(instr);
-        }
-    }
-
-    fn remap_instruction(&self, instr: &mut Instruction) {
-        match instr {
-            Instruction::Call(i) => {
-                self.remap_idx(i);
-            }
-            _ => {}
-        }
-    }
-
-    fn remap_name_section(&self, names: &mut NameSection) {
-        match names {
-            NameSection::Function(f) => self.remap_function_name_section(f),
-            NameSection::Local(f) => self.remap_local_name_section(f),
-            _ => {}
-        }
-    }
-
-    fn remap_function_name_section(&self, names: &mut FunctionNameSection) {
-        let map = names.names_mut();
-        let new = IndexMap::with_capacity(map.len());
-        for (mut idx, name) in mem::replace(map, new) {
-            if !self.remap_idx(&mut idx) {
-                map.insert(idx, name);
-            }
-        }
-    }
-
-    fn remap_local_name_section(&self, names: &mut LocalNameSection) {
-        let map = names.local_names_mut();
-        let new = IndexMap::with_capacity(map.len());
-        for (mut idx, name) in mem::replace(map, new) {
-            if !self.remap_idx(&mut idx) {
-                map.insert(idx, name);
-            }
-        }
-    }
-
-    /// Returns whether `idx` pointed to a previously known descriptor function
-    /// that we're switching to an import
-    fn remap_idx(&self, idx: &mut u32) -> bool {
-        // If this was an imported function we didn't reorder those, so nothing
-        // to do.
-        if *idx < self.old_num_imports {
-            return false;
-        }
-        // ... otherwise we're injecting a number of new imports, so offset
-        // everything.
-        *idx += self.code_idx_to_descriptor.len() as u32;
-        false
-    }
-}
diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs
index d4850484..382ab39a 100644
--- a/crates/cli-support/src/js/mod.rs
+++ b/crates/cli-support/src/js/mod.rs
@@ -11,6 +11,7 @@ use shared;
 use super::Bindgen;
 use descriptor::{Descriptor, VectorKind};
 use wasm_interpreter::Interpreter;
+use wasm_utils::Remap;
 
 mod js2rust;
 use self::js2rust::Js2Rust;
@@ -30,6 +31,7 @@ pub struct Context<'a> {
     pub imported_statics: HashSet<&'a str>,
     pub config: &'a Bindgen,
     pub module: &'a mut Module,
+    pub start: Option<String>,
 
     /// A map which maintains a list of what identifiers we've imported and what
     /// they're named locally.
@@ -163,7 +165,6 @@ impl<'a> Context<'a> {
     }
 
     pub fn finalize(&mut self, module_name: &str) -> Result<(String, String), Error> {
-        self.parse_wasm_names();
         self.write_classes()?;
 
         self.bind("__wbindgen_object_clone_ref", &|me| {
@@ -460,6 +461,37 @@ impl<'a> Context<'a> {
 
         self.unexport_unused_internal_exports();
         closures::rewrite(self)?;
+
+        // Handle the `start` function, if one was specified. If we're in a
+        // --test mode (such as wasm-bindgen-test-runner) then we skip this
+        // entirely. Otherwise we want to first add a start function to the
+        // `start` section if one is specified.
+        //
+        // Afterwards, we need to perform what's a bit of a hack. Right after we
+        // added the start function, we remove it again because no current
+        // strategy for bundlers and deployment works well enough with it. For
+        // `--no-modules` output we need to be sure to call the start function
+        // after our exports are wired up (or most imported functions won't
+        // work).
+        //
+        // For ESM outputs bundlers like webpack also don't work because
+        // currently they run the wasm initialization before the JS glue
+        // initialization, meaning that if the wasm start function calls
+        // imported functions the JS glue isn't ready to go just yet.
+        //
+        // To handle `--no-modules` we just unstart the start function and call
+        // it manually. To handle the ESM use case we switch the start function
+        // to calling an imported function which defers the start function via
+        // `Promise.resolve().then(...)` to execute on the next microtask tick.
+        let mut has_start_function = false;
+        if self.config.emit_start {
+            self.add_start_function()?;
+            has_start_function = self.unstart_start_function();
+            if has_start_function && !self.config.no_modules {
+                self.inject_start_shim();
+            }
+        }
+
         self.gc();
 
         // Note that it's important `throw` comes last *after* we gc. The
@@ -537,6 +569,7 @@ impl<'a> Context<'a> {
             init.__wbindgen_wasm_instance = instance;
             init.__wbindgen_wasm_module = module;
             init.__wbindgen_wasm_memory = __exports.memory;
+            {start}
         }});
     }};
     self.{global_name} = Object.assign(init, __exports);
@@ -550,6 +583,11 @@ impl<'a> Context<'a> {
                     .map(|s| &**s)
                     .unwrap_or("wasm_bindgen"),
                 init_memory = memory,
+                start = if has_start_function {
+                    "wasm.__wbindgen_start();"
+                } else {
+                    ""
+                },
             )
         } else if self.config.no_modules {
             format!(
@@ -578,7 +616,7 @@ impl<'a> Context<'a> {
         }}
         return instantiation.then(({{instance}}) => {{
             wasm = init.wasm = instance.exports;
-            return;
+            {start}
         }});
     }};
     self.{global_name} = Object.assign(init, __exports);
@@ -591,6 +629,11 @@ impl<'a> Context<'a> {
                     .as_ref()
                     .map(|s| &**s)
                     .unwrap_or("wasm_bindgen"),
+                start = if has_start_function {
+                    "wasm.__wbindgen_start();"
+                } else {
+                    ""
+                },
             )
         } else {
             let import_wasm = if self.globals.len() == 0 {
@@ -1774,7 +1817,7 @@ impl<'a> Context<'a> {
             .run(&mut self.module);
     }
 
-    fn parse_wasm_names(&mut self) {
+    pub fn parse_wasm_names(&mut self) {
         let module = mem::replace(self.module, Module::default());
         let module = module.parse_names().unwrap_or_else(|p| p.1);
         *self.module = module;
@@ -2148,6 +2191,128 @@ impl<'a> Context<'a> {
             Ok(())
         }
     }
+
+    fn add_start_function(&mut self) -> Result<(), Error> {
+        let start = match &self.start {
+            Some(name) => name.clone(),
+            None => return Ok(()),
+        };
+        let idx = {
+            let exports = self.module.export_section()
+                .ok_or_else(|| format_err!("no export section found"))?;
+            let entry = exports
+                .entries()
+                .iter()
+                .find(|e| e.field() == start)
+                .ok_or_else(|| format_err!("export `{}` not found", start))?;
+            match entry.internal() {
+                Internal::Function(i) => *i,
+                _ => bail!("export `{}` wasn't a function", start),
+            }
+        };
+        if let Some(prev_start) = self.module.start_section() {
+            if let Some(NameSection::Function(n)) = self.module.names_section() {
+                if let Some(prev) = n.names().get(prev_start) {
+                    bail!("cannot flag `{}` as start function as `{}` is \
+                           already the start function", start, prev);
+                }
+            }
+
+            bail!("cannot flag `{}` as start function as another \
+                   function is already the start function", start);
+        }
+
+        self.set_start_section(idx);
+        Ok(())
+    }
+
+    fn set_start_section(&mut self, start: u32) {
+        let mut pos = None;
+        // See http://webassembly.github.io/spec/core/binary/modules.html#binary-module
+        // for section ordering
+        for (i, section) in self.module.sections().iter().enumerate() {
+            match section {
+                Section::Type(_) |
+                Section::Import(_) |
+                Section::Function(_) |
+                Section::Table(_) |
+                Section::Memory(_) |
+                Section::Global(_) |
+                Section::Export(_) => continue,
+                _ => {
+                    pos = Some(i);
+                    break
+                }
+            }
+        }
+        let pos = pos.unwrap_or(self.module.sections().len() - 1);
+        self.module.sections_mut().insert(pos, Section::Start(start));
+    }
+
+    /// If a start function is present, it removes it from the `start` section
+    /// of the wasm module and then moves it to an exported function, named
+    /// `__wbindgen_start`.
+    fn unstart_start_function(&mut self) -> bool {
+        let mut pos = None;
+        let mut start = 0;
+        for (i, section) in self.module.sections().iter().enumerate() {
+            if let Section::Start(idx) = section {
+                start = *idx;
+                pos = Some(i);
+                break;
+            }
+        }
+        match pos {
+            Some(i) => {
+                self.module.sections_mut().remove(i);
+                let entry = ExportEntry::new(
+                    "__wbindgen_start".to_string(),
+                    Internal::Function(start),
+                );
+                self.module.export_section_mut().unwrap().entries_mut().push(entry);
+                true
+            }
+            None => false,
+        }
+    }
+
+    /// Injects a `start` function into the wasm module. This start function
+    /// calls a shim in the generated JS which defers the actual start function
+    /// to the next microtask tick of the event queue.
+    ///
+    /// See docs above at callsite for why this happens.
+    fn inject_start_shim(&mut self) {
+        let body = "function() {
+            Promise.resolve().then(() => wasm.__wbindgen_start());
+        }";
+        self.export("__wbindgen_defer_start", body, None);
+
+        let imports = self.module.import_section()
+            .map(|s| s.functions() as u32)
+            .unwrap_or(0);
+        Remap(|idx| {
+            if idx < imports { idx } else { idx + 1 }
+        }).remap_module(self.module);
+
+        let type_idx = {
+            let types = self.module.type_section_mut().unwrap();
+            let ty = Type::Function(FunctionType::new(Vec::new(), None));
+            types.types_mut().push(ty);
+            (types.types_mut().len() - 1) as u32
+        };
+
+        let entry = ImportEntry::new(
+            "__wbindgen_placeholder__".to_string(),
+            "__wbindgen_defer_start".to_string(),
+            External::Function(type_idx),
+        );
+        self.module
+            .import_section_mut()
+            .unwrap()
+            .entries_mut()
+            .push(entry);
+        self.set_start_section(imports);
+    }
 }
 
 impl<'a, 'b> SubContext<'a, 'b> {
@@ -2184,6 +2349,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
 
     fn generate_export(&mut self, export: &decode::Export<'b>) -> Result<(), Error> {
         if let Some(ref class) = export.class {
+            assert!(!export.start);
             return self.generate_export_for_class(class, export);
         }
 
@@ -2192,6 +2358,10 @@ impl<'a, 'b> SubContext<'a, 'b> {
             Some(d) => d,
         };
 
+        if export.start {
+            self.set_start_function(export.function.name)?;
+        }
+
         let (js, ts, js_doc) = Js2Rust::new(&export.function.name, self.cx)
             .process(descriptor.unwrap_function())?
             .finish("function", &format!("wasm.{}", export.function.name));
@@ -2207,6 +2377,15 @@ impl<'a, 'b> SubContext<'a, 'b> {
         Ok(())
     }
 
+    fn set_start_function(&mut self, start: &str) -> Result<(), Error> {
+        if let Some(prev) = &self.cx.start {
+            bail!("cannot flag `{}` as start function as `{}` is \
+                   already the start function", start, prev);
+        }
+        self.cx.start = Some(start.to_string());
+        Ok(())
+    }
+
     fn generate_export_for_class(
         &mut self,
         class_name: &'b str,
diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs
index 14a543c3..f5e46dc0 100644
--- a/crates/cli-support/src/lib.rs
+++ b/crates/cli-support/src/lib.rs
@@ -22,6 +22,7 @@ use parity_wasm::elements::*;
 mod decode;
 mod descriptor;
 mod js;
+mod wasm_utils;
 pub mod wasm2es6js;
 
 pub struct Bindgen {
@@ -36,6 +37,7 @@ pub struct Bindgen {
     demangle: bool,
     keep_debug: bool,
     remove_name_section: bool,
+    emit_start: bool,
     // Experimental support for `WeakRefGroup`, an upcoming ECMAScript feature.
     // Currently only enable-able through an env var.
     weak_refs: bool,
@@ -64,6 +66,7 @@ impl Bindgen {
             demangle: true,
             keep_debug: false,
             remove_name_section: false,
+            emit_start: true,
             weak_refs: env::var("WASM_BINDGEN_WEAKREF").is_ok(),
             threads: threads_config(),
         }
@@ -131,6 +134,11 @@ impl Bindgen {
         self
     }
 
+    pub fn emit_start(&mut self, emit: bool) -> &mut Bindgen {
+        self.emit_start = emit;
+        self
+    }
+
     pub fn generate<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
         self._generate(path.as_ref())
     }
@@ -195,7 +203,9 @@ impl Bindgen {
                 imported_functions: Default::default(),
                 imported_statics: Default::default(),
                 direct_imports: Default::default(),
+                start: None,
             };
+            cx.parse_wasm_names();
             for program in programs.iter() {
                 js::SubContext {
                     program,
diff --git a/crates/cli-support/src/wasm_utils.rs b/crates/cli-support/src/wasm_utils.rs
new file mode 100644
index 00000000..3d81857c
--- /dev/null
+++ b/crates/cli-support/src/wasm_utils.rs
@@ -0,0 +1,104 @@
+use std::mem;
+
+use parity_wasm::elements::*;
+
+pub struct Remap<F>(pub F);
+
+impl<F> Remap<F> where F: FnMut(u32) -> u32 {
+    pub fn remap_module(&mut self, module: &mut Module) {
+        for section in module.sections_mut() {
+            match section {
+                Section::Export(e) => self.remap_export_section(e),
+                Section::Element(e) => self.remap_element_section(e),
+                Section::Code(e) => self.remap_code_section(e),
+                Section::Start(i) => {
+                    self.remap_idx(i);
+                }
+                Section::Name(n) => self.remap_name_section(n),
+                _ => {}
+            }
+        }
+    }
+
+    fn remap_export_section(&mut self, section: &mut ExportSection) {
+        for entry in section.entries_mut() {
+            self.remap_export_entry(entry);
+        }
+    }
+
+    fn remap_export_entry(&mut self, entry: &mut ExportEntry) {
+        match entry.internal_mut() {
+            Internal::Function(i) => {
+                self.remap_idx(i);
+            }
+            _ => {}
+        }
+    }
+
+    fn remap_element_section(&mut self, section: &mut ElementSection) {
+        for entry in section.entries_mut() {
+            self.remap_element_entry(entry);
+        }
+    }
+
+    fn remap_element_entry(&mut self, entry: &mut ElementSegment) {
+        for member in entry.members_mut() {
+            self.remap_idx(member);
+        }
+    }
+
+    fn remap_code_section(&mut self, section: &mut CodeSection) {
+        for body in section.bodies_mut() {
+            self.remap_func_body(body);
+        }
+    }
+
+    fn remap_func_body(&mut self, body: &mut FuncBody) {
+        self.remap_instructions(body.code_mut());
+    }
+
+    fn remap_instructions(&mut self, code: &mut Instructions) {
+        for instr in code.elements_mut() {
+            self.remap_instruction(instr);
+        }
+    }
+
+    fn remap_instruction(&mut self, instr: &mut Instruction) {
+        match instr {
+            Instruction::Call(i) => {
+                self.remap_idx(i);
+            }
+            _ => {}
+        }
+    }
+
+    fn remap_name_section(&mut self, names: &mut NameSection) {
+        match names {
+            NameSection::Function(f) => self.remap_function_name_section(f),
+            NameSection::Local(f) => self.remap_local_name_section(f),
+            _ => {}
+        }
+    }
+
+    fn remap_function_name_section(&mut self, names: &mut FunctionNameSection) {
+        let map = names.names_mut();
+        let new = IndexMap::with_capacity(map.len());
+        for (mut idx, name) in mem::replace(map, new) {
+            self.remap_idx(&mut idx);
+            map.insert(idx, name);
+        }
+    }
+
+    fn remap_local_name_section(&mut self, names: &mut LocalNameSection) {
+        let map = names.local_names_mut();
+        let new = IndexMap::with_capacity(map.len());
+        for (mut idx, name) in mem::replace(map, new) {
+            self.remap_idx(&mut idx);
+            map.insert(idx, name);
+        }
+    }
+
+    fn remap_idx(&mut self, idx: &mut u32) {
+        *idx = (self.0)(*idx);
+    }
+}
diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
index 9bd8b672..522f6310 100644
--- a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
+++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
@@ -132,6 +132,7 @@ fn rmain() -> Result<(), Error> {
         .nodejs(node)
         .input_module(module, wasm)
         .keep_debug(false)
+        .emit_start(false)
         .generate(&tmpdir)
         .context("executing `wasm-bindgen` over the wasm file")?;
     shell.clear();
diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs
index d11d77d1..d5d1c6ac 100644
--- a/crates/macro-support/src/parser.rs
+++ b/crates/macro-support/src/parser.rs
@@ -47,6 +47,7 @@ macro_rules! attrgen {
             (vendor_prefix, VendorPrefix(Span, Ident)),
             (variadic, Variadic(Span)),
             (typescript_custom_section, TypescriptCustomSection(Span)),
+            (start, Start(Span)),
         }
     )
 }
@@ -730,6 +731,20 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
                 let comments = extract_doc_comments(&f.attrs);
                 f.to_tokens(tokens);
                 let opts = opts.unwrap_or_default();
+                if opts.start().is_some() {
+                    if f.decl.generics.params.len() > 0 {
+                        bail_span!(
+                            &f.decl.generics,
+                            "the start function cannot have generics",
+                        );
+                    }
+                    if f.decl.inputs.len() > 0 {
+                        bail_span!(
+                            &f.decl.inputs,
+                            "the start function cannot have arguments",
+                        );
+                    }
+                }
                 program.exports.push(ast::Export {
                     rust_class: None,
                     js_class: None,
@@ -737,6 +752,7 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
                     is_constructor: false,
                     comments,
                     rust_name: f.ident.clone(),
+                    start: opts.start().is_some(),
                     function: f.convert(opts)?,
                 });
             }
@@ -898,6 +914,7 @@ impl<'a, 'b> MacroParse<&'a BindgenAttrs> for (&'a Ident, &'b mut syn::ImplItem)
             is_constructor,
             function,
             comments,
+            start: false,
             rust_name: method.sig.ident.clone(),
         });
         opts.check_used()?;
diff --git a/crates/macro/ui-tests/start-function.rs b/crates/macro/ui-tests/start-function.rs
new file mode 100644
index 00000000..de7e51d0
--- /dev/null
+++ b/crates/macro/ui-tests/start-function.rs
@@ -0,0 +1,12 @@
+extern crate wasm_bindgen;
+
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(start)]
+pub fn foo() {}
+
+#[wasm_bindgen(start)]
+pub fn foo2(x: u32) {}
+
+#[wasm_bindgen(start)]
+pub fn foo3<T>() {}
diff --git a/crates/macro/ui-tests/start-function.stderr b/crates/macro/ui-tests/start-function.stderr
new file mode 100644
index 00000000..0b772187
--- /dev/null
+++ b/crates/macro/ui-tests/start-function.stderr
@@ -0,0 +1,14 @@
+error: the start function cannot have arguments
+ --> $DIR/start-function.rs:9:13
+  |
+9 | pub fn foo2(x: u32) {}
+  |             ^^^^^^
+
+error: the start function cannot have generics
+  --> $DIR/start-function.rs:12:12
+   |
+12 | pub fn foo3<T>() {}
+   |            ^^^
+
+error: aborting due to 2 previous errors
+
diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs
index 5cb245c6..546620b5 100644
--- a/crates/shared/src/lib.rs
+++ b/crates/shared/src/lib.rs
@@ -84,6 +84,7 @@ macro_rules! shared_api {
             is_constructor: bool,
             function: Function<'a>,
             comments: Vec<&'a str>,
+            start: bool,
         }
 
         struct Enum<'a> {
diff --git a/examples/canvas/index.js b/examples/canvas/index.js
index 3439a276..4b586d9a 100644
--- a/examples/canvas/index.js
+++ b/examples/canvas/index.js
@@ -1,5 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example.
 import('./canvas')
-  .then(canvas => canvas.draw())
   .catch(console.error);
diff --git a/examples/canvas/src/lib.rs b/examples/canvas/src/lib.rs
index 3e31f1ba..9d03a478 100644
--- a/examples/canvas/src/lib.rs
+++ b/examples/canvas/src/lib.rs
@@ -6,8 +6,8 @@ use std::f64;
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 
-#[wasm_bindgen]
-pub fn draw() {
+#[wasm_bindgen(start)]
+pub fn start() {
     let document = web_sys::window().unwrap().document().unwrap();
     let canvas = document.get_element_by_id("canvas").unwrap();
     let canvas: web_sys::HtmlCanvasElement = canvas
diff --git a/examples/closures/index.js b/examples/closures/index.js
index 26cd9d48..cc27e057 100644
--- a/examples/closures/index.js
+++ b/examples/closures/index.js
@@ -1,6 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example
-const rust = import('./closures');
-rust
-  .then(m => m.run())
+import('./closures')
   .catch(console.error);
diff --git a/examples/closures/src/lib.rs b/examples/closures/src/lib.rs
index a12d7b52..30a28380 100644
--- a/examples/closures/src/lib.rs
+++ b/examples/closures/src/lib.rs
@@ -7,7 +7,7 @@ use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 use web_sys::{Document, Element, HtmlElement, Window};
 
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
 pub fn run() -> Result<(), JsValue> {
     let window = web_sys::window().expect("should have a window in this context");
     let document = window.document().expect("window should have a document");
diff --git a/examples/console_log/index.js b/examples/console_log/index.js
index 39d597af..824c1371 100644
--- a/examples/console_log/index.js
+++ b/examples/console_log/index.js
@@ -1,7 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example
-const rust = import('./console_log');
-
-rust
-  .then(m => m.run())
+import('./console_log')
   .catch(console.error);
diff --git a/examples/console_log/src/lib.rs b/examples/console_log/src/lib.rs
index b03de921..a530259a 100644
--- a/examples/console_log/src/lib.rs
+++ b/examples/console_log/src/lib.rs
@@ -3,7 +3,7 @@ extern crate web_sys;
 
 use wasm_bindgen::prelude::*;
 
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
 pub fn run() {
     bare_bones();
     using_a_macro();
diff --git a/examples/dom/index.js b/examples/dom/index.js
index 2447c51c..0d74f2fa 100644
--- a/examples/dom/index.js
+++ b/examples/dom/index.js
@@ -1,6 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example
-const rust = import('./dom');
-rust
-  .then(m => m.run())
+import('./dom')
   .catch(console.error);
diff --git a/examples/dom/src/lib.rs b/examples/dom/src/lib.rs
index a30af3bc..fc6a9628 100644
--- a/examples/dom/src/lib.rs
+++ b/examples/dom/src/lib.rs
@@ -4,7 +4,7 @@ extern crate web_sys;
 use wasm_bindgen::prelude::*;
 
 // Called by our JS entry point to run the example
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
 pub fn run() -> Result<(), JsValue> {
     // Use `web_sys`'s global `window` function to get a handle on the global
     // window object.
diff --git a/examples/import_js/index.js b/examples/import_js/index.js
index 5b2ef4ec..9d7e3b1a 100644
--- a/examples/import_js/index.js
+++ b/examples/import_js/index.js
@@ -1,7 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example
-const rust = import('./import_js');
-
-rust
-  .then(m => m.run())
+import('./import_js')
   .catch(console.error);
diff --git a/examples/import_js/src/lib.rs b/examples/import_js/src/lib.rs
index 6ea0cbd0..9033e808 100644
--- a/examples/import_js/src/lib.rs
+++ b/examples/import_js/src/lib.rs
@@ -26,7 +26,7 @@ extern "C" {
     fn log(s: &str);
 }
 
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
 pub fn run() {
     log(&format!("Hello, {}!", name()));
 
diff --git a/examples/no_modules/index.html b/examples/no_modules/index.html
index 90147d23..50193bc3 100644
--- a/examples/no_modules/index.html
+++ b/examples/no_modules/index.html
@@ -27,7 +27,6 @@
           // initialization and return to us a promise when it's done
           // also, we can use 'await' on the returned promise
           await wasm_bindgen('./no_modules_bg.wasm');
-          wasm_bindgen.run();
       });
     </script>
   </body>
diff --git a/examples/no_modules/src/lib.rs b/examples/no_modules/src/lib.rs
index a30af3bc..7bed7348 100644
--- a/examples/no_modules/src/lib.rs
+++ b/examples/no_modules/src/lib.rs
@@ -4,8 +4,8 @@ extern crate web_sys;
 use wasm_bindgen::prelude::*;
 
 // Called by our JS entry point to run the example
-#[wasm_bindgen]
-pub fn run() -> Result<(), JsValue> {
+#[wasm_bindgen(start)]
+pub fn main() -> Result<(), JsValue> {
     // Use `web_sys`'s global `window` function to get a handle on the global
     // window object.
     let window = web_sys::window().expect("no global `window` exists");
diff --git a/examples/paint/index.js b/examples/paint/index.js
index 6d88e810..6775ddb4 100644
--- a/examples/paint/index.js
+++ b/examples/paint/index.js
@@ -1,5 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example.
 import('./wasm_bindgen_paint')
-  .then(paint => paint.main())
   .catch(console.error);
diff --git a/examples/paint/src/lib.rs b/examples/paint/src/lib.rs
index c69a9b67..cdd92333 100644
--- a/examples/paint/src/lib.rs
+++ b/examples/paint/src/lib.rs
@@ -7,8 +7,8 @@ use std::rc::Rc;
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 
-#[wasm_bindgen]
-pub fn main() -> Result<(), JsValue> {
+#[wasm_bindgen(start)]
+pub fn start() -> Result<(), JsValue> {
     let document = web_sys::window().unwrap().document().unwrap();
     let canvas = document
         .create_element("canvas")?
diff --git a/examples/performance/index.js b/examples/performance/index.js
index 48e692d7..0c7c097c 100644
--- a/examples/performance/index.js
+++ b/examples/performance/index.js
@@ -1,6 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example
-const rust = import('./performance');
-rust
-  .then(m => m.run())
+import('./performance')
   .catch(console.error);
diff --git a/examples/performance/src/lib.rs b/examples/performance/src/lib.rs
index 0dad2a6b..dad7fad5 100644
--- a/examples/performance/src/lib.rs
+++ b/examples/performance/src/lib.rs
@@ -17,8 +17,7 @@ macro_rules! console_log {
     ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
 }
 
-// Called by our JS entry point to run the example
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
 pub fn run() {
     let window = web_sys::window().expect("should have a window in this context");
     let performance = window
diff --git a/examples/wasm-in-wasm/index.js b/examples/wasm-in-wasm/index.js
index e68f8a60..a8df06e5 100644
--- a/examples/wasm-in-wasm/index.js
+++ b/examples/wasm-in-wasm/index.js
@@ -1,6 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example
-const rust = import('./wasm_in_wasm');
-rust
-  .then(m => m.run())
+import('./wasm_in_wasm')
   .catch(console.error);
diff --git a/examples/wasm-in-wasm/src/lib.rs b/examples/wasm-in-wasm/src/lib.rs
index 3496a3e7..2aecb909 100644
--- a/examples/wasm-in-wasm/src/lib.rs
+++ b/examples/wasm-in-wasm/src/lib.rs
@@ -18,7 +18,7 @@ macro_rules! console_log {
 
 const WASM: &[u8] = include_bytes!("add.wasm");
 
-#[wasm_bindgen]
+#[wasm_bindgen(start)]
 pub fn run() -> Result<(), JsValue> {
     console_log!("instantiating a new wasm module directly");
     let my_memory = wasm_bindgen::memory()
diff --git a/examples/webgl/index.js b/examples/webgl/index.js
index f717a293..dc67e610 100644
--- a/examples/webgl/index.js
+++ b/examples/webgl/index.js
@@ -1,5 +1,4 @@
 // For more comments about what's going on here, check out the `hello_world`
 // example.
 import('./webgl')
-  .then(webgl => webgl.draw())
   .catch(console.error);
diff --git a/examples/webgl/src/lib.rs b/examples/webgl/src/lib.rs
index 16bcf557..1916ee7c 100644
--- a/examples/webgl/src/lib.rs
+++ b/examples/webgl/src/lib.rs
@@ -7,8 +7,8 @@ use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 use web_sys::{WebGlProgram, WebGlRenderingContext, WebGlShader};
 
-#[wasm_bindgen]
-pub fn draw() -> Result<(), JsValue> {
+#[wasm_bindgen(start)]
+pub fn start() -> Result<(), JsValue> {
     let document = web_sys::window().unwrap().document().unwrap();
     let canvas = document.get_element_by_id("canvas").unwrap();
     let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into::<web_sys::HtmlCanvasElement>()?;
diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md
index 26b36caf..5ed5b1e5 100644
--- a/guide/src/SUMMARY.md
+++ b/guide/src/SUMMARY.md
@@ -76,6 +76,7 @@
       - [`constructor`](./reference/attributes/on-rust-exports/constructor.md)
       - [`js_name = Blah`](./reference/attributes/on-rust-exports/js_name.md)
       - [`readonly`](./reference/attributes/on-rust-exports/readonly.md)
+      - [`start`](./reference/attributes/on-rust-exports/start.md)
       - [`typescript_custom_section`](./reference/attributes/on-rust-exports/typescript_custom_section.md)
 
 --------------------------------------------------------------------------------
diff --git a/guide/src/reference/attributes/on-rust-exports/start.md b/guide/src/reference/attributes/on-rust-exports/start.md
new file mode 100644
index 00000000..8dd52f73
--- /dev/null
+++ b/guide/src/reference/attributes/on-rust-exports/start.md
@@ -0,0 +1,31 @@
+# `start`
+
+When attached to a `pub` function this attribute will configure the `start`
+section of the wasm executable to be emitted, executing the tagged function as
+soon as the wasm module is instantiated.
+
+```rust
+#[wasm_bindgen(start)]
+pub fn main() {
+    // executed automatically ...
+}
+```
+
+The `start` section of the wasm executable will be configured to execute the
+`main` function here as soon as it can. Note that due to various practical
+limitations today the start section of the executable may not literally point to
+`main`, but the `main` function here should be started up automatically when the
+wasm module is loaded.
+
+There's a few caveats to be aware of when using the `start` attribute:
+
+* The `start` function must take no arguments and must either return `()` or
+  `Result<(), JsValue>`
+* Only one `start` function can be placed into a module, including its
+  dependencies. If more than one is specified then `wasm-bindgen` will fail when
+  the CLI is run. It's recommended that only applications use this attribute.
+* The `start` function will not be executed when testing.
+* If you're experimenting with WebAssembly threads, the `start` function is
+  executed *once per thread*, not once globally!
+* Note that the `start` function is relatively new, so if you find any bugs with
+  it, please feel free to report an issue!
diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs
index e0793a78..283ecbd5 100644
--- a/tests/wasm/main.rs
+++ b/tests/wasm/main.rs
@@ -10,6 +10,8 @@ extern crate wasm_bindgen_test_crate_b;
 #[macro_use]
 extern crate serde_derive;
 
+use wasm_bindgen::prelude::*;
+
 pub mod api;
 pub mod char;
 pub mod classes;
@@ -36,3 +38,9 @@ pub mod u64;
 pub mod validate_prt;
 pub mod variadic;
 pub mod vendor_prefix;
+
+// should not be executed
+#[wasm_bindgen(start)]
+pub fn start() {
+    panic!();
+}