Repository: rustwasm/wasm-bindgen
Pull request #4056 (2024-08-06 14:00:06)

Improve the CI runtime
Follow-up to #4051, see there for prior discussion.

This splits up the longest-running CI jobs into smaller jobs to improve the CI runtime.

The overall _usage_ didn't go down much, it's still around 100 minutes, but now it's spread out over more jobs.
(It's gotten a bit more stable slightly under the 100 minute mark with this PR, but not much.)

Additional changes are commented below.

daxpedda: I'm happy to give it a final review already as-is, up to you when its ready!

I believe the last three things we could do are:
- Parallelize `build_examples` via bash somehow (or if you know your way around webpack maybe there is something there?).
- Remove the `test --all-features -p web-sys`, it can be tested without all features, but we have to add the required features to dev-dependencies. Checking building with all features is already done in another part of the CI.
- Cross-compile `dist_windows` from Linux, which should finally get us down to under 3 minutes (if the other two points work out!).

Could all be done in a follow-up btw.

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 02e750ec..fcc080ee 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -5,6 +5,7 @@ on:
     tags-ignore: [dev]
   pull_request:
     branches: [main]
+  workflow_dispatch:
 defaults:
   run:
     shell: bash
@@ -53,7 +54,6 @@ jobs:
     - run: cargo clippy --no-deps --all-features -p example-tests -- -D warnings
     - run: cargo clippy --no-deps --all-features -p wasm-bindgen-externref-xform -- -D warnings
     - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -p wasm-bindgen-futures -- -D warnings
-    - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -p js-sys -- -D warnings
     - run: cargo clippy --no-deps --all-features -p wasm-bindgen-macro -- -D warnings
     - run: cargo clippy --no-deps --all-features -p wasm-bindgen-macro-support -- -D warnings
     - run: cargo clippy --no-deps --all-features -p wasm-bindgen-multi-value-xform -- -D warnings
@@ -66,11 +66,30 @@ jobs:
     - run: cargo clippy --no-deps --all-features -p wasm-bindgen-wasm-interpreter -- -D warnings
     - run: cargo clippy --no-deps --all-features -p wasm-bindgen-webidl -- -D warnings
     - run: cargo clippy --no-deps --all-features -p webidl-tests -- -D warnings
+    - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -p wasm-bindgen-benchmark -- -D warnings
+
+  # Run `cargo clippy` over web-sys and js-sys crates
+  clippy_web_sys:
+    name: Clippy (web-sys)
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
+    - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -p js-sys -- -D warnings
     - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -p web-sys -- -D warnings
+
+  # Run `cargo clippy` over the project
+  clippy_project:
+    name: Clippy (project)
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
     - run: cargo clippy --no-deps --no-default-features --target wasm32-unknown-unknown -- -D warnings
     - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -- -D warnings
     - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown --tests -- -D warnings
-    - run: cargo clippy --no-deps --all-features --target wasm32-unknown-unknown -p wasm-bindgen-benchmark -- -D warnings
     - run: for i in examples/*/; do cd "$i"; cargo +stable clippy --no-deps --all-features --target wasm32-unknown-unknown -- -D warnings || exit 1; cd ../..; done
 
   test_wasm_bindgen:
@@ -87,12 +106,52 @@ jobs:
         node-version: '20'
     - uses: ./.github/actions/setup-geckodriver
     - run: cargo test --target wasm32-unknown-unknown
+    - run: cargo test --target wasm32-unknown-unknown -p wasm-bindgen-futures
+
+  test_wasm_bindgen_features:
+    name: "Run wasm-bindgen crate tests with different features"
+    runs-on: ubuntu-latest
+    env:
+      WASM_BINDGEN_SPLIT_LINKED_MODULES: 1
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+    - uses: ./.github/actions/setup-geckodriver
     - run: cargo test --target wasm32-unknown-unknown --features serde-serialize
     - run: cargo test --target wasm32-unknown-unknown --features enable-interning
-    - run: cargo test --target wasm32-unknown-unknown -p wasm-bindgen-futures
+
+  test_wasm_bindgen_wasm:
+    name: "Run wasm-bindgen wasm test"
+    runs-on: ubuntu-latest
+    env:
+      WASM_BINDGEN_SPLIT_LINKED_MODULES: 1
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
     - run: cargo test --target wasm32-unknown-unknown --test wasm
       env:
         WASM_BINDGEN_NO_DEBUG: 1
+
+  test_wasm_bindgen_envs:
+    name: "Run wasm-bindgen crate tests with various environment variables"
+    runs-on: ubuntu-latest
+    env:
+      WASM_BINDGEN_SPLIT_LINKED_MODULES: 1
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
     - run: cargo test --target wasm32-unknown-unknown
       env:
         WASM_BINDGEN_EXTERNREF: 1
@@ -171,7 +230,31 @@ jobs:
     - run: cargo build --manifest-path crates/web-sys/Cargo.toml --target wasm32-unknown-unknown --features Node
     - run: cargo build --manifest-path crates/web-sys/Cargo.toml --target wasm32-unknown-unknown --features Element
     - run: cargo build --manifest-path crates/web-sys/Cargo.toml --target wasm32-unknown-unknown --features Window
+
+  test_web_sys_all_features:
+    name: "Run web-sys crate tests with all features"
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+    - uses: ./.github/actions/setup-geckodriver
     - run: cargo test --manifest-path crates/web-sys/Cargo.toml --target wasm32-unknown-unknown --all-features
+
+  test_web_sys_all_features_unstable:
+    name: "Run web-sys crate tests with all features and unstable APIs"
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - run: rustup update --no-self-update stable && rustup default stable
+    - run: rustup target add wasm32-unknown-unknown
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+    - uses: ./.github/actions/setup-geckodriver
     - run: cargo test --manifest-path crates/web-sys/Cargo.toml --target wasm32-unknown-unknown --all-features
       env:
         RUSTFLAGS: --cfg=web_sys_unstable_apis
@@ -448,7 +531,7 @@ jobs:
     - uses: actions/checkout@v4
     - run: rustup update --no-self-update 1.57 && rustup default 1.57
     - run: cargo build
-      
+
   msrv-cli:
     name: Check MSRV for CLI tools
     runs-on: ubuntu-latest
@@ -459,12 +542,13 @@ jobs:
     - uses: actions/checkout@v4
     - run: rustup update --no-self-update 1.76 && rustup default 1.76
     - run: cargo build
-      
+
 
   deploy:
     permissions:
       contents: write #  to push changes in repo (jamesives/github-pages-deploy-action)
 
+    if: github.repository == 'rustwasm/wasm-bindgen'
     needs:
       - doc_api
       - doc_book
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9f9997d..df7ad8a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -72,7 +72,7 @@
 * Added an experimental Node.JS ES module target, in comparison the current `node` target uses CommonJS, with `--target experimental-nodejs-module` or when testing with `wasm_bindgen_test_configure!(run_in_node_experimental)`.
   [#4027](https://github.com/rustwasm/wasm-bindgen/pull/4027)
 
-* Added importing strings as `JsString` through `#[wasm_bindgen(static_string)] static STRING: JsString = "a string literal";`.
+* Added importing strings as `JsString` through `#[wasm_bindgen(thread_local, static_string)] static STRING: JsString = "a string literal";`.
   [#4055](https://github.com/rustwasm/wasm-bindgen/pull/4055)
 
 ### Changed
@@ -129,6 +129,12 @@
 * Filtered files in published crates, significantly reducing the package size and notably excluding any bash files.
   [#4046](https://github.com/rustwasm/wasm-bindgen/pull/4046)
 
+* Deprecated `JsStatic` in favor of `#[wasm_bindgen(thread_local)]`, which creates a `std::thread::LocalKey`. The syntax is otherwise the same.
+  [#4057](https://github.com/rustwasm/wasm-bindgen/pull/4057)
+
+* Removed `impl Deref for JsStatic` when compiling with `cfg(target_feature = "atomics")`, which was unsound.
+  [#4057](https://github.com/rustwasm/wasm-bindgen/pull/4057)
+
 ### Fixed
 
 * Copy port from headless test server when using `WASM_BINDGEN_TEST_ADDRESS`.
diff --git a/_package.json b/_package.json
index f69515ae..d347502d 100644
--- a/_package.json
+++ b/_package.json
@@ -5,12 +5,11 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "css-loader": "^6.8.1",
-    "html-webpack-plugin": "^5.3.2",
-    "mini-css-extract-plugin": "^2.7.6",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^3.11.2"
+    "css-loader": "^7.1.2",
+    "html-webpack-plugin": "^5.6.0",
+    "mini-css-extract-plugin": "^2.9.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "5.0.4"
   }
 }
diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index 3474d3a5..e10cf756 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -275,6 +275,8 @@ pub struct ImportStatic {
     pub js_name: String,
     /// Path to wasm_bindgen
     pub wasm_bindgen: Path,
+    /// [`true`] if using the new `thread_local` representation.
+    pub thread_local: bool,
 }
 
 /// The type of a static string being imported
diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs
index c826d67c..4add9859 100644
--- a/crates/backend/src/codegen.rs
+++ b/crates/backend/src/codegen.rs
@@ -1645,15 +1645,42 @@ impl ToTokens for ast::Enum {
 impl ToTokens for ast::ImportStatic {
     fn to_tokens(&self, into: &mut TokenStream) {
         let ty = &self.ty;
-        static_import(
-            &self.vis,
-            &self.rust_name,
-            &self.wasm_bindgen,
-            ty,
-            ty,
-            &self.shim,
-        )
-        .to_tokens(into);
+
+        if self.thread_local {
+            thread_local_import(
+                &self.vis,
+                &self.rust_name,
+                &self.wasm_bindgen,
+                ty,
+                ty,
+                &self.shim,
+            )
+            .to_tokens(into)
+        } else {
+            let vis = &self.vis;
+            let name = &self.rust_name;
+            let wasm_bindgen = &self.wasm_bindgen;
+            let ty = &self.ty;
+            let shim_name = &self.shim;
+            let init = static_init(wasm_bindgen, ty, shim_name);
+
+            into.extend(quote! {
+                #[automatically_derived]
+                #[deprecated = "use with `#[wasm_bindgen(thread_local)]` instead"]
+            });
+            into.extend(
+                quote_spanned! { name.span() => #vis static #name: #wasm_bindgen::JsStatic<#ty> = {
+                        fn init() -> #ty {
+                            #init
+                        }
+                        thread_local!(static _VAL: #ty = init(););
+                        #wasm_bindgen::JsStatic {
+                            __inner: &_VAL,
+                        }
+                    };
+                },
+            );
+        }
 
         Descriptor {
             ident: &self.shim,
@@ -1672,7 +1699,7 @@ impl ToTokens for ast::ImportString {
         let js_sys = &self.js_sys;
         let actual_ty: syn::Type = parse_quote!(#js_sys::JsString);
 
-        static_import(
+        thread_local_import(
             &self.vis,
             &self.rust_name,
             &self.wasm_bindgen,
@@ -1684,7 +1711,7 @@ impl ToTokens for ast::ImportString {
     }
 }
 
-fn static_import(
+fn thread_local_import(
     vis: &syn::Visibility,
     name: &Ident,
     wasm_bindgen: &syn::Path,
@@ -1692,33 +1719,37 @@ fn static_import(
     ty: &syn::Type,
     shim_name: &Ident,
 ) -> TokenStream {
+    let init = static_init(wasm_bindgen, ty, shim_name);
+
+    quote! {
+        thread_local! {
+            #[automatically_derived]
+            #vis static #name: #actual_ty = {
+                #init
+            };
+        }
+    }
+}
+
+fn static_init(wasm_bindgen: &syn::Path, ty: &syn::Type, shim_name: &Ident) -> TokenStream {
     let abi_ret = quote! {
         #wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi>
     };
     quote! {
-        #[automatically_derived]
-        #vis static #name: #wasm_bindgen::JsStatic<#actual_ty> = {
-            fn init() -> #ty {
-                #[link(wasm_import_module = "__wbindgen_placeholder__")]
-                #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
-                extern "C" {
-                    fn #shim_name() -> #abi_ret;
-                }
+        #[link(wasm_import_module = "__wbindgen_placeholder__")]
+        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+        extern "C" {
+            fn #shim_name() -> #abi_ret;
+        }
 
-                #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
-                unsafe fn #shim_name() -> #abi_ret {
-                    panic!("cannot access imported statics on non-wasm targets")
-                }
+        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+        unsafe fn #shim_name() -> #abi_ret {
+            panic!("cannot access imported statics on non-wasm targets")
+        }
 
-                unsafe {
-                    <#ty as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#shim_name().join())
-                }
-            }
-            thread_local!(static _VAL: #ty = init(););
-            #wasm_bindgen::JsStatic {
-                __inner: &_VAL,
-            }
-        };
+        unsafe {
+            <#ty as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#shim_name().join())
+        }
     }
 }
 
diff --git a/crates/js-sys/src/lib.rs b/crates/js-sys/src/lib.rs
index 3c1196b7..a9f57d0c 100644
--- a/crates/js-sys/src/lib.rs
+++ b/crates/js-sys/src/lib.rs
@@ -2835,7 +2835,7 @@ macro_rules! number_try_from {
             #[inline]
             fn try_from(x: $x) -> Result<Number, Self::Error> {
                 let x_f64 = x as f64;
-                if x_f64 >= Number::MIN_SAFE_INTEGER && x_f64 <= Number::MAX_SAFE_INTEGER {
+                if (Number::MIN_SAFE_INTEGER..=Number::MAX_SAFE_INTEGER).contains(&x_f64) {
                     Ok(Number::from(x_f64))
                 } else {
                     Err(TryFromIntError(()))
diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs
index 3e3f3113..bcc4d739 100644
--- a/crates/macro-support/src/parser.rs
+++ b/crates/macro-support/src/parser.rs
@@ -93,6 +93,7 @@ macro_rules! attrgen {
             (typescript_type, TypeScriptType(Span, String, Span)),
             (getter_with_clone, GetterWithClone(Span)),
             (static_string, StaticString(Span)),
+            (thread_local, ThreadLocal(Span)),
 
             // For testing purposes only.
             (assert_no_shim, AssertNoShim(Span)),
@@ -753,6 +754,13 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             bail_span!(self.mutability, "cannot import mutable globals yet")
         }
 
+        if let Some(span) = opts.static_string() {
+            return Err(Diagnostic::span_error(
+                *span,
+                "static strings require a string literal",
+            ));
+        }
+
         let default_name = self.ident.to_string();
         let js_name = opts
             .js_name()
@@ -764,6 +772,8 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             self.ident,
             ShortHash((&js_name, module, &self.ident)),
         );
+        let thread_local = opts.thread_local().is_some();
+
         opts.check_used();
         Ok(ast::ImportKind::Static(ast::ImportStatic {
             ty: *self.ty,
@@ -772,6 +782,7 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             js_name,
             shim: Ident::new(&shim, Span::call_site()),
             wasm_bindgen: program.wasm_bindgen.clone(),
+            thread_local,
         }))
     }
 }
@@ -805,7 +816,14 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
         if opts.static_string().is_none() {
             bail_span!(
                 self,
-                "statics strings require `#[wasm_bindgen(static_string)]`"
+                "static strings require `#[wasm_bindgen(static_string)]`"
+            )
+        }
+
+        if opts.thread_local().is_none() {
+            bail_span!(
+                self,
+                "static strings require `#[wasm_bindgen(thread_local)]`"
             )
         }
 
diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml
index cd04f2a6..e448a0d2 100644
--- a/crates/macro/Cargo.toml
+++ b/crates/macro/Cargo.toml
@@ -26,6 +26,7 @@ wasm-bindgen-macro-support = { path = "../macro-support", version = "=0.2.92" }
 quote = "1.0"
 
 [dev-dependencies]
+js-sys = { path = "../js-sys" }
 trybuild = "1.0"
 wasm-bindgen = { path = "../.." }
 wasm-bindgen-futures = { path = "../futures" }
diff --git a/examples/add/package.json b/examples/add/package.json
index 8332534c..5dc619b4 100644
--- a/examples/add/package.json
+++ b/examples/add/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/add/webpack.config.js b/examples/add/webpack.config.js
index 0e3fcd8b..84d67fcf 100644
--- a/examples/add/webpack.config.js
+++ b/examples/add/webpack.config.js
@@ -14,12 +14,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/canvas/package.json b/examples/canvas/package.json
index 8332534c..5dc619b4 100644
--- a/examples/canvas/package.json
+++ b/examples/canvas/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/canvas/webpack.config.js b/examples/canvas/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/canvas/webpack.config.js
+++ b/examples/canvas/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/char/package.json b/examples/char/package.json
index 8332534c..5dc619b4 100644
--- a/examples/char/package.json
+++ b/examples/char/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/char/webpack.config.js b/examples/char/webpack.config.js
index b75134c3..3b17bdb6 100644
--- a/examples/char/webpack.config.js
+++ b/examples/char/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/closures/package.json b/examples/closures/package.json
index 8332534c..5dc619b4 100644
--- a/examples/closures/package.json
+++ b/examples/closures/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/closures/webpack.config.js b/examples/closures/webpack.config.js
index 0d85b517..aac81329 100644
--- a/examples/closures/webpack.config.js
+++ b/examples/closures/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/console_log/package.json b/examples/console_log/package.json
index 8332534c..5dc619b4 100644
--- a/examples/console_log/package.json
+++ b/examples/console_log/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/console_log/webpack.config.js b/examples/console_log/webpack.config.js
index 0e3fcd8b..84d67fcf 100644
--- a/examples/console_log/webpack.config.js
+++ b/examples/console_log/webpack.config.js
@@ -14,12 +14,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/dom/package.json b/examples/dom/package.json
index 8332534c..5dc619b4 100644
--- a/examples/dom/package.json
+++ b/examples/dom/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/dom/webpack.config.js b/examples/dom/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/dom/webpack.config.js
+++ b/examples/dom/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/duck-typed-interfaces/package.json b/examples/duck-typed-interfaces/package.json
index 8332534c..5dc619b4 100644
--- a/examples/duck-typed-interfaces/package.json
+++ b/examples/duck-typed-interfaces/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/duck-typed-interfaces/webpack.config.js b/examples/duck-typed-interfaces/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/duck-typed-interfaces/webpack.config.js
+++ b/examples/duck-typed-interfaces/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/fetch/package.json b/examples/fetch/package.json
index 8332534c..5dc619b4 100644
--- a/examples/fetch/package.json
+++ b/examples/fetch/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/fetch/webpack.config.js b/examples/fetch/webpack.config.js
index 0e3fcd8b..84d67fcf 100644
--- a/examples/fetch/webpack.config.js
+++ b/examples/fetch/webpack.config.js
@@ -14,12 +14,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/guide-supported-types-examples/package.json b/examples/guide-supported-types-examples/package.json
index 8332534c..5dc619b4 100644
--- a/examples/guide-supported-types-examples/package.json
+++ b/examples/guide-supported-types-examples/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/guide-supported-types-examples/webpack.config.js b/examples/guide-supported-types-examples/webpack.config.js
index 0e3fcd8b..84d67fcf 100644
--- a/examples/guide-supported-types-examples/webpack.config.js
+++ b/examples/guide-supported-types-examples/webpack.config.js
@@ -14,12 +14,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/hello_world/package.json b/examples/hello_world/package.json
index 8332534c..5dc619b4 100644
--- a/examples/hello_world/package.json
+++ b/examples/hello_world/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/hello_world/webpack.config.js b/examples/hello_world/webpack.config.js
index 0e3fcd8b..84d67fcf 100644
--- a/examples/hello_world/webpack.config.js
+++ b/examples/hello_world/webpack.config.js
@@ -14,12 +14,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/import_js/package.json b/examples/import_js/package.json
index 8332534c..5dc619b4 100644
--- a/examples/import_js/package.json
+++ b/examples/import_js/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/import_js/webpack.config.js b/examples/import_js/webpack.config.js
index fa5fce23..45d8bde8 100644
--- a/examples/import_js/webpack.config.js
+++ b/examples/import_js/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, "crate")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/julia_set/package.json b/examples/julia_set/package.json
index 8332534c..5dc619b4 100644
--- a/examples/julia_set/package.json
+++ b/examples/julia_set/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/julia_set/webpack.config.js b/examples/julia_set/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/julia_set/webpack.config.js
+++ b/examples/julia_set/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/paint/package.json b/examples/paint/package.json
index 8332534c..5dc619b4 100644
--- a/examples/paint/package.json
+++ b/examples/paint/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/paint/webpack.config.js b/examples/paint/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/paint/webpack.config.js
+++ b/examples/paint/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/performance/package.json b/examples/performance/package.json
index 8332534c..5dc619b4 100644
--- a/examples/performance/package.json
+++ b/examples/performance/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/performance/webpack.config.js b/examples/performance/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/performance/webpack.config.js
+++ b/examples/performance/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/request-animation-frame/package.json b/examples/request-animation-frame/package.json
index 8b08e487..e9feb6b1 100644
--- a/examples/request-animation-frame/package.json
+++ b/examples/request-animation-frame/package.json
@@ -1,14 +1,13 @@
 {
   "scripts": {
     "build": "webpack",
-    "serve" : "webpack serve"
+    "serve": "webpack serve"
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "text-encoding": "^0.7.0",
-    "html-webpack-plugin": "^5.3.2",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^3.11.2"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^3.11.3"
   }
 }
diff --git a/examples/request-animation-frame/webpack.config.js b/examples/request-animation-frame/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/request-animation-frame/webpack.config.js
+++ b/examples/request-animation-frame/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json
index 8d72e6a9..889bc2b6 100644
--- a/examples/todomvc/package.json
+++ b/examples/todomvc/package.json
@@ -5,12 +5,11 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "css-loader": "^6.8.1",
-    "html-webpack-plugin": "^5.3.2",
-    "mini-css-extract-plugin": "^2.7.6",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "css-loader": "^6.11.0",
+    "html-webpack-plugin": "^5.6.0",
+    "mini-css-extract-plugin": "^2.9.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js
index 27eafe32..cee2b6b7 100644
--- a/examples/todomvc/webpack.config.js
+++ b/examples/todomvc/webpack.config.js
@@ -18,12 +18,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/wasm-audio-worklet/src/dependent_module.rs b/examples/wasm-audio-worklet/src/dependent_module.rs
index ce7f038f..5e074d72 100644
--- a/examples/wasm-audio-worklet/src/dependent_module.rs
+++ b/examples/wasm-audio-worklet/src/dependent_module.rs
@@ -12,7 +12,7 @@ extern "C" {
     #[wasm_bindgen(method, getter)]
     fn url(this: &ImportMeta) -> JsString;
 
-    #[wasm_bindgen(js_namespace = import, js_name = meta)]
+    #[wasm_bindgen(thread_local, js_namespace = import, js_name = meta)]
     static IMPORT_META: ImportMeta;
 }
 
@@ -20,7 +20,7 @@ pub fn on_the_fly(code: &str) -> Result<String, JsValue> {
     // Generate the import of the bindgen ES module, assuming `--target web`.
     let header = format!(
         "import init, * as bindgen from '{}';\n\n",
-        IMPORT_META.url(),
+        IMPORT_META.with(ImportMeta::url),
     );
 
     let options = BlobPropertyBag::new();
diff --git a/examples/wasm-in-wasm-imports/package.json b/examples/wasm-in-wasm-imports/package.json
index 8332534c..5dc619b4 100644
--- a/examples/wasm-in-wasm-imports/package.json
+++ b/examples/wasm-in-wasm-imports/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/wasm-in-wasm-imports/webpack.config.js b/examples/wasm-in-wasm-imports/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/wasm-in-wasm-imports/webpack.config.js
+++ b/examples/wasm-in-wasm-imports/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/wasm-in-wasm/package.json b/examples/wasm-in-wasm/package.json
index 8332534c..5dc619b4 100644
--- a/examples/wasm-in-wasm/package.json
+++ b/examples/wasm-in-wasm/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/wasm-in-wasm/webpack.config.js b/examples/wasm-in-wasm/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/wasm-in-wasm/webpack.config.js
+++ b/examples/wasm-in-wasm/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/weather_report/package.json b/examples/weather_report/package.json
index 8332534c..5dc619b4 100644
--- a/examples/weather_report/package.json
+++ b/examples/weather_report/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/weather_report/webpack.config.js b/examples/weather_report/webpack.config.js
index 25625457..7e6b1687 100644
--- a/examples/weather_report/webpack.config.js
+++ b/examples/weather_report/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-            TextDecoder: ['text-encoding', 'TextDecoder'],
-            TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/webaudio/package.json b/examples/webaudio/package.json
index 8332534c..5dc619b4 100644
--- a/examples/webaudio/package.json
+++ b/examples/webaudio/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/webaudio/webpack.config.js b/examples/webaudio/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/webaudio/webpack.config.js
+++ b/examples/webaudio/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/webgl/package.json b/examples/webgl/package.json
index 8332534c..5dc619b4 100644
--- a/examples/webgl/package.json
+++ b/examples/webgl/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/webgl/webpack.config.js b/examples/webgl/webpack.config.js
index 18329afd..7db80e9d 100644
--- a/examples/webgl/webpack.config.js
+++ b/examples/webgl/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
       new WasmPackPlugin({
           crateDirectory: path.resolve(__dirname, ".")
       }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/webrtc_datachannel/package.json b/examples/webrtc_datachannel/package.json
index 8332534c..5dc619b4 100644
--- a/examples/webrtc_datachannel/package.json
+++ b/examples/webrtc_datachannel/package.json
@@ -5,10 +5,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/webrtc_datachannel/webpack.config.js b/examples/webrtc_datachannel/webpack.config.js
index 95559a44..b1d31d4f 100644
--- a/examples/webrtc_datachannel/webpack.config.js
+++ b/examples/webrtc_datachannel/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
         new WasmPackPlugin({
             crateDirectory: path.resolve(__dirname, ".")
         }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/examples/webxr/package.json b/examples/webxr/package.json
index 91598ef3..ec5924d6 100644
--- a/examples/webxr/package.json
+++ b/examples/webxr/package.json
@@ -8,10 +8,9 @@
   },
   "devDependencies": {
     "@wasm-tool/wasm-pack-plugin": "1.5.0",
-    "html-webpack-plugin": "^5.3.2",
-    "text-encoding": "^0.7.0",
-    "webpack": "^5.49.0",
-    "webpack-cli": "^4.7.2",
-    "webpack-dev-server": "^4.15.1"
+    "html-webpack-plugin": "^5.6.0",
+    "webpack": "^5.93.0",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   }
 }
diff --git a/examples/webxr/webpack.config.js b/examples/webxr/webpack.config.js
index 18329afd..7db80e9d 100644
--- a/examples/webxr/webpack.config.js
+++ b/examples/webxr/webpack.config.js
@@ -16,12 +16,6 @@ module.exports = {
       new WasmPackPlugin({
           crateDirectory: path.resolve(__dirname, ".")
       }),
-        // Have this example work in Edge which doesn't ship `TextEncoder` or
-        // `TextDecoder` at this time.
-        new webpack.ProvidePlugin({
-          TextDecoder: ['text-encoding', 'TextDecoder'],
-          TextEncoder: ['text-encoding', 'TextEncoder']
-        })
     ],
     mode: 'development',
     experiments: {
diff --git a/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md b/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md
index 5dad7d46..0d0443cb 100644
--- a/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md
+++ b/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md
@@ -60,7 +60,8 @@ on methods:
 #[wasm_bindgen]
 extern "C" {
     type Foo;
-    static foo: Foo;
+    #[wasm_bindgen(thread_local)]
+    static FOO: Foo;
 
     #[wasm_bindgen(method, structural, indexing_getter)]
     fn get(this: &Foo, prop: &str) -> u32;
@@ -72,11 +73,13 @@ extern "C" {
     fn delete(this: &Foo, prop: &str);
 }
 
-assert_eq!(foo.get("ten"), 3);
+FOO.with(|foo| {
+    assert_eq!(foo.get("ten"), 3);
 
-foo.set("ten", 10);
-assert_eq!(foo.get("ten"), 10);
+    foo.set("ten", 10);
+    assert_eq!(foo.get("ten"), 10);
 
-foo.delete("ten");
-assert_eq!(foo.get("ten"), 3);
+    foo.delete("ten");
+    assert_eq!(foo.get("ten"), 3);
+});
 ```
diff --git a/guide/src/reference/static-js-objects.md b/guide/src/reference/static-js-objects.md
index 9c15fcbb..2402098e 100644
--- a/guide/src/reference/static-js-objects.md
+++ b/guide/src/reference/static-js-objects.md
@@ -19,11 +19,12 @@ let COLORS = {
 ```rust
 #[wasm_bindgen]
 extern "C" {
+    #[wasm_bindgen(thread_local)]
     static COLORS;
 }
 
 fn get_colors() -> JsValue {
-    COLORS.clone()
+    COLORS.with(JsValue::clone)
 }
 ```
 
@@ -49,11 +50,11 @@ The binding for this module:
 #[wasm_bindgen(module = "/js/some-rollup.js")]
 extern "C" {
     // Likewise with the namespace--this refers to the object directly.
-    #[wasm_bindgen(js_name = namespace)]
+    #[wasm_bindgen(thread_local, js_name = namespace)]
     static NAMESPACE: JsValue;
 
     // Refer to SomeType's class
-    #[wasm_bindgen(js_name = SomeType)]
+    #[wasm_bindgen(thread_local, js_name = SomeType)]
     static SOME_TYPE: JsValue;
 
     // Other bindings for SomeType
@@ -70,7 +71,7 @@ Strings can be imported to avoid going through `TextDecoder/Encoder` when requir
 ```rust
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(static_string)]
+    #[wasm_bindgen(thread_local, static_string)]
     static STRING: JsString = "a string literal";
 }
 ```
diff --git a/src/lib.rs b/src/lib.rs
index a6914269..e8e30b9a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1185,23 +1185,18 @@ impl Default for JsValue {
 /// This type implements `Deref` to the inner type so it's typically used as if
 /// it were `&T`.
 #[cfg(feature = "std")]
+#[deprecated = "use with `#[wasm_bindgen(thread_local)]` instead"]
 pub struct JsStatic<T: 'static> {
     #[doc(hidden)]
     pub __inner: &'static std::thread::LocalKey<T>,
 }
 
 #[cfg(feature = "std")]
+#[allow(deprecated)]
+#[cfg(not(target_feature = "atomics"))]
 impl<T: FromWasmAbi + 'static> Deref for JsStatic<T> {
     type Target = T;
     fn deref(&self) -> &T {
-        // We know that our tls key is never overwritten after initialization,
-        // so it should be safe (on that axis at least) to hand out a reference
-        // that lives longer than the closure below.
-        //
-        // FIXME: this is not sound if we ever implement thread exit hooks on
-        // wasm, as the pointer will eventually be invalidated but you can get
-        // `&'static T` from this interface. We... probably need to deprecate
-        // and/or remove this interface nowadays.
         unsafe { self.__inner.with(|ptr| &*(ptr as *const T)) }
     }
 }


Test patch:
diff --git a/crates/js-sys/tests/wasm/Function.rs b/crates/js-sys/tests/wasm/Function.rs
index c87a46cc..f0f27dca 100644
--- a/crates/js-sys/tests/wasm/Function.rs
+++ b/crates/js-sys/tests/wasm/Function.rs
@@ -5,13 +5,13 @@ use wasm_bindgen_test::*;
 
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(js_name = max, js_namespace = Math)]
+    #[wasm_bindgen(thread_local, js_name = max, js_namespace = Math)]
     static MAX: Function;
 
     type ArrayPrototype;
     #[wasm_bindgen(method, getter, structural)]
     pub fn push(this: &ArrayPrototype) -> Function;
-    #[wasm_bindgen(js_name = prototype, js_namespace = Array)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Array)]
     static ARRAY_PROTOTYPE2: ArrayPrototype;
 }
 
@@ -21,12 +21,19 @@ fn apply() {
     args.push(&1.into());
     args.push(&2.into());
     args.push(&3.into());
-    assert_eq!(MAX.apply(&JsValue::undefined(), &args).unwrap(), 3);
+    assert_eq!(
+        MAX.with(|max| max.apply(&JsValue::undefined(), &args))
+            .unwrap(),
+        3
+    );
 
     let arr = JsValue::from(Array::new());
     let args = Array::new();
     args.push(&1.into());
-    ARRAY_PROTOTYPE2.push().apply(&arr, &args).unwrap();
+    ARRAY_PROTOTYPE2
+        .with(ArrayPrototype::push)
+        .apply(&arr, &args)
+        .unwrap();
     assert_eq!(Array::from(&arr).length(), 1);
 }
 
@@ -111,24 +118,29 @@ fn bind3() {
 
 #[wasm_bindgen_test]
 fn length() {
-    assert_eq!(MAX.length(), 2);
-    assert_eq!(ARRAY_PROTOTYPE2.push().length(), 1);
+    assert_eq!(MAX.with(Function::length), 2);
+    assert_eq!(ARRAY_PROTOTYPE2.with(ArrayPrototype::push).length(), 1);
 }
 
 #[wasm_bindgen_test]
 fn name() {
-    assert_eq!(JsValue::from(MAX.name()), "max");
-    assert_eq!(JsValue::from(ARRAY_PROTOTYPE2.push().name()), "push");
+    assert_eq!(JsValue::from(MAX.with(Function::name)), "max");
+    assert_eq!(
+        JsValue::from(ARRAY_PROTOTYPE2.with(ArrayPrototype::push).name()),
+        "push"
+    );
 }
 
 #[wasm_bindgen_test]
 fn to_string() {
-    assert!(MAX.to_string().length() > 0);
+    assert!(MAX.with(Function::to_string).length() > 0);
 }
 
 #[wasm_bindgen_test]
 fn function_inheritance() {
-    assert!(MAX.is_instance_of::<Function>());
-    assert!(MAX.is_instance_of::<Object>());
-    let _: &Object = MAX.as_ref();
+    assert!(MAX.with(Function::is_instance_of::<Function>));
+    assert!(MAX.with(Function::is_instance_of::<Object>));
+    MAX.with(|max| {
+        let _: &Object = max.as_ref();
+    });
 }
diff --git a/crates/js-sys/tests/wasm/Object.rs b/crates/js-sys/tests/wasm/Object.rs
index 69db1af3..b97c31d0 100644
--- a/crates/js-sys/tests/wasm/Object.rs
+++ b/crates/js-sys/tests/wasm/Object.rs
@@ -9,9 +9,9 @@ extern "C" {
     #[wasm_bindgen(method, setter, structural)]
     fn set_foo(this: &Foo42, val: JsValue);
 
-    #[wasm_bindgen(js_name = prototype, js_namespace = Object)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Object)]
     static OBJECT_PROTOTYPE: JsValue;
-    #[wasm_bindgen(js_name = prototype, js_namespace = Array)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Array)]
     static ARRAY_PROTOTYPE: JsValue;
 
     type DefinePropertyAttrs;
@@ -32,9 +32,9 @@ extern "C" {
     #[wasm_bindgen(constructor)]
     fn new() -> Foo;
 
-    #[wasm_bindgen(js_name = prototype, js_namespace = Foo)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Foo)]
     static FOO_PROTOTYPE: Object;
-    #[wasm_bindgen(js_name = prototype, js_namespace = Bar)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Bar)]
     static BAR_PROTOTYPE: Object;
 }
 
@@ -178,9 +178,9 @@ fn get_own_property_symbols() {
 #[wasm_bindgen_test]
 fn get_prototype_of() {
     let proto = JsValue::from(Object::get_prototype_of(&Object::new().into()));
-    assert_eq!(proto, *OBJECT_PROTOTYPE);
+    OBJECT_PROTOTYPE.with(|op| assert_eq!(proto, *op));
     let proto = JsValue::from(Object::get_prototype_of(&Array::new().into()));
-    assert_eq!(proto, *ARRAY_PROTOTYPE);
+    ARRAY_PROTOTYPE.with(|ap| assert_eq!(proto, *ap));
 }
 
 #[wasm_bindgen_test]
@@ -249,8 +249,8 @@ fn is_sealed() {
 #[wasm_bindgen_test]
 fn is_prototype_of() {
     let foo = JsValue::from(Foo::new());
-    assert!(FOO_PROTOTYPE.is_prototype_of(&foo));
-    assert!(!BAR_PROTOTYPE.is_prototype_of(&foo));
+    assert!(FOO_PROTOTYPE.with(|fp| fp.is_prototype_of(&foo)));
+    assert!(!BAR_PROTOTYPE.with(|bp| bp.is_prototype_of(&foo)));
 }
 
 #[wasm_bindgen_test]
diff --git a/crates/macro/ui-tests/invalid-items.rs b/crates/macro/ui-tests/invalid-items.rs
index f1a35633..9ab441c0 100644
--- a/crates/macro/ui-tests/invalid-items.rs
+++ b/crates/macro/ui-tests/invalid-items.rs
@@ -10,15 +10,26 @@ pub const fn foo2() {}
 struct Foo<T>(T);
 
 #[wasm_bindgen]
+#[rustfmt::skip]
 extern "C" {
     static mut FOO: u32;
 
+    #[wasm_bindgen(static_string)]
+    static FOO2: JsString;
+
+    #[wasm_bindgen(thread_local, static_string)]
+    static FOO3: JsString;
+
+    static FOO4: JsString = "test";
+
+    #[wasm_bindgen(static_string)]
+    static FOO5: JsString = "test";
+
     pub fn foo3(x: i32, ...);
 }
 
 #[wasm_bindgen]
-extern "system" {
-}
+extern "system" {}
 
 #[wasm_bindgen]
 pub fn foo4<T>() {}
diff --git a/crates/macro/ui-tests/invalid-items.stderr b/crates/macro/ui-tests/invalid-items.stderr
index 1800ade2..f985c96c 100644
--- a/crates/macro/ui-tests/invalid-items.stderr
+++ b/crates/macro/ui-tests/invalid-items.stderr
@@ -17,43 +17,67 @@ error: structs with #[wasm_bindgen] cannot have lifetime or type parameters curr
    |           ^^^
 
 error: cannot import mutable globals yet
-  --> $DIR/invalid-items.rs:14:12
+  --> $DIR/invalid-items.rs:15:12
    |
-14 |     static mut FOO: u32;
+15 |     static mut FOO: u32;
    |            ^^^
 
+error: static strings require a string literal
+  --> $DIR/invalid-items.rs:17:20
+   |
+17 |     #[wasm_bindgen(static_string)]
+   |                    ^^^^^^^^^^^^^
+
+error: static strings require a string literal
+  --> $DIR/invalid-items.rs:20:34
+   |
+20 |     #[wasm_bindgen(thread_local, static_string)]
+   |                                  ^^^^^^^^^^^^^
+
+error: static strings require `#[wasm_bindgen(static_string)]`
+  --> $DIR/invalid-items.rs:23:5
+   |
+23 |     static FOO4: JsString = "test";
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: static strings require `#[wasm_bindgen(thread_local)]`
+  --> $DIR/invalid-items.rs:26:5
+   |
+26 |     static FOO5: JsString = "test";
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
 error: can't #[wasm_bindgen] variadic functions
-  --> $DIR/invalid-items.rs:16:25
+  --> $DIR/invalid-items.rs:28:25
    |
-16 |     pub fn foo3(x: i32, ...);
+28 |     pub fn foo3(x: i32, ...);
    |                         ^^^
 
 error: only foreign mods with the `C` ABI are allowed
-  --> $DIR/invalid-items.rs:20:8
+  --> $DIR/invalid-items.rs:32:8
    |
-20 | extern "system" {
+32 | extern "system" {}
    |        ^^^^^^^^
 
 error: can't #[wasm_bindgen] functions with lifetime or type parameters
-  --> $DIR/invalid-items.rs:24:12
+  --> $DIR/invalid-items.rs:35:12
    |
-24 | pub fn foo4<T>() {}
+35 | pub fn foo4<T>() {}
    |            ^^^
 
 error: can't #[wasm_bindgen] functions with lifetime or type parameters
-  --> $DIR/invalid-items.rs:26:12
+  --> $DIR/invalid-items.rs:37:12
    |
-26 | pub fn foo5<'a>() {}
+37 | pub fn foo5<'a>() {}
    |            ^^^^
 
 error: can't #[wasm_bindgen] functions with lifetime or type parameters
-  --> $DIR/invalid-items.rs:28:12
+  --> $DIR/invalid-items.rs:39:12
    |
-28 | pub fn foo6<'a, T>() {}
+39 | pub fn foo6<'a, T>() {}
    |            ^^^^^^^
 
 error: #[wasm_bindgen] can only be applied to a function, struct, enum, impl, or extern block
-  --> $DIR/invalid-items.rs:31:1
+  --> $DIR/invalid-items.rs:42:1
    |
-31 | trait X {}
+42 | trait X {}
    | ^^^^^^^^^^
diff --git a/crates/macro/ui-tests/invalid-static-string.rs b/crates/macro/ui-tests/invalid-static-string.rs
new file mode 100644
index 00000000..7b50b107
--- /dev/null
+++ b/crates/macro/ui-tests/invalid-static-string.rs
@@ -0,0 +1,10 @@
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+#[rustfmt::skip]
+extern "C" {
+    #[wasm_bindgen(thread_local, static_string)]
+    static FOO: JsValue = "test";
+}
+
+fn main() {}
diff --git a/crates/macro/ui-tests/invalid-static-string.stderr b/crates/macro/ui-tests/invalid-static-string.stderr
new file mode 100644
index 00000000..7d1872a1
--- /dev/null
+++ b/crates/macro/ui-tests/invalid-static-string.stderr
@@ -0,0 +1,14 @@
+error[E0308]: mismatched types
+ --> $DIR/invalid-static-string.rs:3:1
+  |
+3 | #[wasm_bindgen]
+  | ^^^^^^^^^^^^^^^
+  | |
+  | expected `JsString`, found `JsValue`
+  | expected `JsString` because of return type
+  |
+  = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
+help: call `Into::into` on this expression to convert `wasm_bindgen::JsValue` into `JsString`
+  |
+3 | #[wasm_bindgen].into()
+  |                +++++++
diff --git a/crates/test/src/rt/browser.rs b/crates/test/src/rt/browser.rs
index fc576f6d..49b27725 100644
--- a/crates/test/src/rt/browser.rs
+++ b/crates/test/src/rt/browser.rs
@@ -19,7 +19,8 @@ pub struct Browser {
 #[wasm_bindgen]
 extern "C" {
     type HTMLDocument;
-    static document: HTMLDocument;
+    #[wasm_bindgen(thread_local, js_name = document)]
+    static DOCUMENT: HTMLDocument;
     #[wasm_bindgen(method, structural)]
     fn getElementById(this: &HTMLDocument, id: &str) -> Element;
 
@@ -38,7 +39,7 @@ impl Browser {
     /// Creates a new instance of `Browser`, assuming that its APIs will work
     /// (requires `Node::new()` to have return `None` first).
     pub fn new() -> Browser {
-        let pre = document.getElementById("output");
+        let pre = DOCUMENT.with(|document| document.getElementById("output"));
         pre.set_text_content("");
         Browser { pre }
     }
diff --git a/crates/web-sys/tests/wasm/history.rs b/crates/web-sys/tests/wasm/history.rs
index f3ecdc42..e2800520 100644
--- a/crates/web-sys/tests/wasm/history.rs
+++ b/crates/web-sys/tests/wasm/history.rs
@@ -4,25 +4,27 @@ use web_sys::{History, ScrollRestoration};
 
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(js_name = history, js_namespace = window)]
+    #[wasm_bindgen(thread_local, js_name = history, js_namespace = window)]
     static HISTORY: History;
 }
 
 #[wasm_bindgen_test]
 fn history() {
-    HISTORY
-        .set_scroll_restoration(ScrollRestoration::Manual)
-        .expect("failure to set scroll restoration");
-    assert_eq!(
-        HISTORY.scroll_restoration().unwrap(),
-        ScrollRestoration::Manual
-    );
+    HISTORY.with(|history| {
+        history
+            .set_scroll_restoration(ScrollRestoration::Manual)
+            .expect("failure to set scroll restoration");
+        assert_eq!(
+            history.scroll_restoration().unwrap(),
+            ScrollRestoration::Manual
+        );
 
-    HISTORY
-        .set_scroll_restoration(ScrollRestoration::Auto)
-        .expect("failure to set scroll restoration");
-    assert_eq!(
-        HISTORY.scroll_restoration().unwrap(),
-        ScrollRestoration::Auto
-    );
+        history
+            .set_scroll_restoration(ScrollRestoration::Auto)
+            .expect("failure to set scroll restoration");
+        assert_eq!(
+            history.scroll_restoration().unwrap(),
+            ScrollRestoration::Auto
+        );
+    });
 }
diff --git a/crates/web-sys/tests/wasm/performance.rs b/crates/web-sys/tests/wasm/performance.rs
index 84931a62..09fb59a6 100644
--- a/crates/web-sys/tests/wasm/performance.rs
+++ b/crates/web-sys/tests/wasm/performance.rs
@@ -4,12 +4,12 @@ use web_sys::Performance;
 
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(js_name = performance)]
+    #[wasm_bindgen(thread_local, js_name = performance)]
     static PERFORMANCE: Performance;
 }
 
 #[wasm_bindgen_test]
 fn to_json() {
-    let perf = JsValue::from(PERFORMANCE.to_json());
+    let perf = JsValue::from(PERFORMANCE.with(Performance::to_json));
     assert!(perf.is_object());
 }
diff --git a/tests/headless/strings.rs b/tests/headless/strings.rs
index 944db5f2..baa551b8 100644
--- a/tests/headless/strings.rs
+++ b/tests/headless/strings.rs
@@ -15,7 +15,7 @@ fn string_roundtrip() {
 
     assert_eq!("\u{feff}bar", &identity("\u{feff}bar"));
 
-    assert_eq!(String::from(&*STRING), "foo")
+    assert_eq!(STRING.with(|s| String::from(s)), "foo");
 }
 
 #[wasm_bindgen]
@@ -23,6 +23,6 @@ fn string_roundtrip() {
 // See <https://github.com/rust-lang/rustfmt/issues/6267>.
 #[rustfmt::skip]
 extern "C" {
-    #[wasm_bindgen(static_string)]
+    #[wasm_bindgen(thread_local, static_string)]
     static STRING: JsString = "foo";
 }
diff --git a/tests/non_wasm.rs b/tests/non_wasm.rs
index dd770732..ca8acad3 100644
--- a/tests/non_wasm.rs
+++ b/tests/non_wasm.rs
@@ -31,13 +31,14 @@ pub fn foo(x: bool) {
 #[wasm_bindgen]
 extern "C" {
     fn some_import();
+    #[wasm_bindgen(thread_local)]
     static A: JsValue;
 }
 
 #[wasm_bindgen]
 pub fn bar(_: &str) -> JsValue {
     some_import();
-    A.clone()
+    A.with(JsValue::clone)
 }
 
 #[wasm_bindgen]
diff --git a/tests/wasm/duplicates.rs b/tests/wasm/duplicates.rs
index 206fede7..348f67b8 100644
--- a/tests/wasm/duplicates.rs
+++ b/tests/wasm/duplicates.rs
@@ -6,7 +6,8 @@ pub mod same_function_different_locations_a {
     #[wasm_bindgen(module = "tests/wasm/duplicates_a.js")]
     extern "C" {
         pub fn foo();
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -16,7 +17,8 @@ pub mod same_function_different_locations_b {
     #[wasm_bindgen(module = "tests/wasm/duplicates_a.js")]
     extern "C" {
         pub fn foo();
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -24,8 +26,8 @@ pub mod same_function_different_locations_b {
 fn same_function_different_locations() {
     same_function_different_locations_a::foo();
     same_function_different_locations_b::foo();
-    assert_eq!(*same_function_different_locations_a::bar, 3);
-    assert_eq!(*same_function_different_locations_a::bar, 3);
+    same_function_different_locations_a::BAR.with(|bar| assert_eq!(*bar, 3));
+    same_function_different_locations_a::BAR.with(|bar| assert_eq!(*bar, 3));
 }
 
 pub mod same_function_different_modules_a {
@@ -34,7 +36,8 @@ pub mod same_function_different_modules_a {
     #[wasm_bindgen(module = "tests/wasm/duplicates_b.js")]
     extern "C" {
         pub fn foo() -> bool;
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -44,7 +47,8 @@ pub mod same_function_different_modules_b {
     #[wasm_bindgen(module = "tests/wasm/duplicates_c.js")]
     extern "C" {
         pub fn foo() -> bool;
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -52,6 +56,6 @@ pub mod same_function_different_modules_b {
 fn same_function_different_modules() {
     assert!(same_function_different_modules_a::foo());
     assert!(!same_function_different_modules_b::foo());
-    assert_eq!(*same_function_different_modules_a::bar, 4);
-    assert_eq!(*same_function_different_modules_b::bar, 5);
+    same_function_different_modules_a::BAR.with(|bar| assert_eq!(*bar, 4));
+    same_function_different_modules_b::BAR.with(|bar| assert_eq!(*bar, 5));
 }
diff --git a/tests/wasm/imports.rs b/tests/wasm/imports.rs
index a9fbb33b..aa5a8b4f 100644
--- a/tests/wasm/imports.rs
+++ b/tests/wasm/imports.rs
@@ -23,6 +23,7 @@ extern "C" {
 
     fn assert_valid_error(val: JsValue);
 
+    #[wasm_bindgen(thread_local)]
     static IMPORT: JsValue;
 
     #[wasm_bindgen(js_name = return_three)]
@@ -35,7 +36,7 @@ extern "C" {
 
     #[allow(non_camel_case_types)]
     type bar;
-    #[wasm_bindgen(js_namespace = bar, js_name = foo)]
+    #[wasm_bindgen(thread_local, js_namespace = bar, js_name = foo)]
     static FOO: JsValue;
 
     fn take_custom_type(f: CustomType) -> CustomType;
@@ -46,7 +47,7 @@ extern "C" {
 
     #[wasm_bindgen(js_name = "baz$")]
     fn renamed_with_dollar_sign();
-    #[wasm_bindgen(js_name = "$foo")]
+    #[wasm_bindgen(thread_local, js_name = "$foo")]
     static RENAMED: JsValue;
 
     fn unused_import();
@@ -57,6 +58,7 @@ extern "C" {
     #[wasm_bindgen(static_method_of = StaticMethodCheck)]
     fn static_method_of_right_this();
 
+    #[wasm_bindgen(thread_local)]
     static STATIC_STRING: String;
 
     #[derive(Clone)]
@@ -170,7 +172,7 @@ fn free_imports() {
 
 #[wasm_bindgen_test]
 fn import_a_field() {
-    assert_eq!(IMPORT.as_f64(), Some(1.0));
+    assert_eq!(IMPORT.with(JsValue::as_f64), Some(1.0));
 }
 
 #[wasm_bindgen_test]
@@ -190,7 +192,7 @@ fn rust_keyword() {
 
 #[wasm_bindgen_test]
 fn rust_keyword2() {
-    assert_eq!(FOO.as_f64(), Some(3.0));
+    assert_eq!(FOO.with(JsValue::as_f64), Some(3.0));
 }
 
 #[wasm_bindgen_test]
@@ -222,7 +224,7 @@ fn rename_with_string() {
 
 #[wasm_bindgen_test]
 fn rename_static_with_string() {
-    assert_eq!(RENAMED.as_f64(), Some(1.0));
+    assert_eq!(RENAMED.with(JsValue::as_f64), Some(1.0));
 }
 
 #[wasm_bindgen_test]
@@ -285,7 +287,7 @@ fn undefined_function_is_ok() {
 
 #[wasm_bindgen_test]
 fn static_string_ok() {
-    assert_eq!(*STATIC_STRING, "x");
+    STATIC_STRING.with(|s| assert_eq!(*s, "x"));
 }
 
 #[wasm_bindgen_test]
diff --git a/tests/wasm/node.rs b/tests/wasm/node.rs
index 8f57e173..59f43eb7 100644
--- a/tests/wasm/node.rs
+++ b/tests/wasm/node.rs
@@ -4,6 +4,7 @@ use wasm_bindgen_test::*;
 #[wasm_bindgen(module = "tests/wasm/node.js")]
 extern "C" {
     fn test_works();
+    #[wasm_bindgen(thread_local)]
     static FOO: JsValue;
     fn hit();
 }
@@ -11,7 +12,7 @@ extern "C" {
 #[wasm_bindgen_test]
 fn works() {
     hit();
-    assert_eq!(FOO.as_f64(), Some(1.0));
+    assert_eq!(FOO.with(JsValue::as_f64), Some(1.0));
     test_works();
 }