diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0467402899..478a1fa981 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,12 @@ Bottom level categories:
 
 ## Unreleased
 
+## v0.17.1
+
+### Added/New Features
+
+- Add `get_mapped_range_as_array_buffer` for faster buffer read-backs in wasm builds. By @ryankaplan in [#4042] (https://github.com/gfx-rs/wgpu/pull/4042).
+
 ## v0.17.0 (2023-07-20)
 
 This is the first release that featured `wgpu-info` as a binary crate for getting information about what devices wgpu sees in your system. It can dump the information in both human readable format and json.
diff --git a/wgpu/src/backend/web.rs b/wgpu/src/backend/web.rs
index 1045041746..3e12fa1d1b 100644
--- a/wgpu/src/backend/web.rs
+++ b/wgpu/src/backend/web.rs
@@ -1890,10 +1890,8 @@ impl crate::context::Context for Context {
         buffer_data: &Self::BufferData,
         sub_range: Range<wgt::BufferAddress>,
     ) -> Box<dyn crate::context::BufferMappedRange> {
-        let array_buffer = buffer_data.0.get_mapped_range_with_f64_and_f64(
-            sub_range.start as f64,
-            (sub_range.end - sub_range.start) as f64,
-        );
+        let array_buffer =
+            self.buffer_get_mapped_range_as_array_buffer(_buffer, buffer_data, sub_range);
         let actual_mapping = js_sys::Uint8Array::new(&array_buffer);
         let temporary_mapping = actual_mapping.to_vec();
         Box::new(BufferMappedRange {
@@ -1902,6 +1900,18 @@ impl crate::context::Context for Context {
         })
     }
 
+    fn buffer_get_mapped_range_as_array_buffer(
+        &self,
+        _buffer: &Self::BufferId,
+        buffer_data: &Self::BufferData,
+        sub_range: Range<wgt::BufferAddress>,
+    ) -> js_sys::ArrayBuffer {
+        buffer_data.0.get_mapped_range_with_f64_and_f64(
+            sub_range.start as f64,
+            (sub_range.end - sub_range.start) as f64,
+        )
+    }
+
     fn buffer_unmap(&self, _buffer: &Self::BufferId, buffer_data: &Self::BufferData) {
         buffer_data.0.unmap();
     }
diff --git a/wgpu/src/context.rs b/wgpu/src/context.rs
index 33e8b5a5e4..74ec6516c3 100644
--- a/wgpu/src/context.rs
+++ b/wgpu/src/context.rs
@@ -307,6 +307,16 @@ pub trait Context: Debug + WasmNotSend + WasmNotSync + Sized {
         buffer_data: &Self::BufferData,
         sub_range: Range<BufferAddress>,
     ) -> Box<dyn BufferMappedRange>;
+    #[cfg(all(
+        target_arch = "wasm32",
+        not(any(target_os = "emscripten", feature = "webgl"))
+    ))]
+    fn buffer_get_mapped_range_as_array_buffer(
+        &self,
+        buffer: &Self::BufferId,
+        buffer_data: &Self::BufferData,
+        sub_range: Range<BufferAddress>,
+    ) -> js_sys::ArrayBuffer;
     fn buffer_unmap(&self, buffer: &Self::BufferId, buffer_data: &Self::BufferData);
     fn texture_create_view(
         &self,
@@ -1375,6 +1385,16 @@ pub(crate) trait DynContext: Debug + WasmNotSend + WasmNotSync {
         buffer_data: &crate::Data,
         sub_range: Range<BufferAddress>,
     ) -> Box<dyn BufferMappedRange>;
+    #[cfg(all(
+        target_arch = "wasm32",
+        not(any(target_os = "emscripten", feature = "webgl"))
+    ))]
+    fn buffer_get_mapped_range_as_array_buffer(
+        &self,
+        buffer: &ObjectId,
+        buffer_data: &crate::Data,
+        sub_range: Range<BufferAddress>,
+    ) -> js_sys::ArrayBuffer;
     fn buffer_unmap(&self, buffer: &ObjectId, buffer_data: &crate::Data);
     fn texture_create_view(
         &self,
@@ -2453,6 +2473,21 @@ where
         Context::buffer_get_mapped_range(self, &buffer, buffer_data, sub_range)
     }
 
+    #[cfg(all(
+        target_arch = "wasm32",
+        not(any(target_os = "emscripten", feature = "webgl"))
+    ))]
+    fn buffer_get_mapped_range_as_array_buffer(
+        &self,
+        buffer: &ObjectId,
+        buffer_data: &crate::Data,
+        sub_range: Range<BufferAddress>,
+    ) -> js_sys::ArrayBuffer {
+        let buffer = <T::BufferId>::from(*buffer);
+        let buffer_data = downcast_ref(buffer_data);
+        Context::buffer_get_mapped_range_as_array_buffer(self, &buffer, buffer_data, sub_range)
+    }
+
     fn buffer_unmap(&self, buffer: &ObjectId, buffer_data: &crate::Data) {
         let buffer = <T::BufferId>::from(*buffer);
         let buffer_data = downcast_ref(buffer_data);
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 0e2be70b09..dc79997b74 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -2931,6 +2931,26 @@ impl<'a> BufferSlice<'a> {
         BufferView { slice: *self, data }
     }
 
+    /// Synchronously and immediately map a buffer for reading. If the buffer is not immediately mappable
+    /// through [`BufferDescriptor::mapped_at_creation`] or [`BufferSlice::map_async`], will panic.
+    ///
+    /// This is useful in wasm builds when you want to pass mapped data directly to js. Unlike `get_mapped_range`
+    /// which unconditionally copies mapped data into the wasm heap, this function directly hands you the
+    /// ArrayBuffer that we mapped the data into in js.
+    #[cfg(all(
+        target_arch = "wasm32",
+        not(any(target_os = "emscripten", feature = "webgl"))
+    ))]
+    pub fn get_mapped_range_as_array_buffer(&self) -> js_sys::ArrayBuffer {
+        let end = self.buffer.map_context.lock().add(self.offset, self.size);
+        DynContext::buffer_get_mapped_range_as_array_buffer(
+            &*self.buffer.context,
+            &self.buffer.id,
+            self.buffer.data.as_ref(),
+            self.offset..end,
+        )
+    }
+
     /// Synchronously and immediately map a buffer for writing. If the buffer is not immediately mappable
     /// through [`BufferDescriptor::mapped_at_creation`] or [`BufferSlice::map_async`], will panic.
     pub fn get_mapped_range_mut(&self) -> BufferViewMut<'a> {