diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4688d7d9..cdf15f22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Added
 
 - A way to customize resolving remote stylesheets.
+- Non-blocking stylesheet resolving by default.
 
 ### Changed
 
diff --git a/README.md b/README.md
index 10325964..746db8c7 100644
--- a/README.md
+++ b/README.md
@@ -70,8 +70,21 @@ const HTML: &str = r#"<html>
 </body>
 </html>"#;
 
+#[tokio::main]
+async fn main() -> css_inline::Result<()> {
+    let inlined = css_inline::inline(HTML).await?;
+    // Do something with inlined HTML, e.g. send an email
+    Ok(())
+}
+```
+
+There is also a "blocking" API for inlining:
+
+```rust
+const HTML: &str = "...";
+
 fn main() -> css_inline::Result<()> {
-    let inlined = css_inline::inline(HTML)?;
+    let inlined = css_inline::blocking::inline(HTML)?;
     // Do something with inlined HTML, e.g. send an email
     Ok(())
 }
@@ -84,11 +97,12 @@ fn main() -> css_inline::Result<()> {
 ```rust
 const HTML: &str = "...";
 
-fn main() -> css_inline::Result<()> {
+#[tokio::main]
+async fn main() -> css_inline::Result<()> {
     let inliner = css_inline::CSSInliner::options()
         .load_remote_stylesheets(false)
         .build();
-    let inlined = inliner.inline(HTML)?;
+    let inlined = inliner.inline(HTML).await?;
     // Do something with inlined HTML, e.g. send an email
     Ok(())
 }
@@ -131,12 +145,28 @@ If you'd like to load stylesheets from your filesystem, use the `file://` scheme
 ```rust
 const HTML: &str = "...";
 
-fn main() -> css_inline::Result<()> {
+#[tokio::main]
+async fn main() -> css_inline::Result<()> {
     let base_url = css_inline::Url::parse("file://styles/email/").expect("Invalid URL");
     let inliner = css_inline::CSSInliner::options()
         .base_url(Some(base_url))
         .build();
-    let inlined = inliner.inline(HTML);
+    let inlined = inliner.inline(HTML).await?;
+    // Do something with inlined HTML, e.g. send an email
+    Ok(())
+}
+```
+
+The blocking version is available as well:
+
+```rust
+const HTML: &str = "...";
+
+fn main() -> css_inline::Result<()> {
+    let inliner = css_inline::blocking::CSSInliner::options()
+        .load_remote_stylesheets(false)
+        .build();
+    let inlined = inliner.inline(HTML)?;
     // Do something with inlined HTML, e.g. send an email
     Ok(())
 }
@@ -149,7 +179,7 @@ For resolving remote stylesheets it is possible to implement a custom resolver:
 pub struct CustomStylesheetResolver;
 
 impl css_inline::StylesheetResolver for CustomStylesheetResolver {
-    fn retrieve(&self, location: &str) -> css_inline::Result<String> {
+    fn retrieve_blocking(&self, location: &str) -> css_inline::Result<String> {
         Err(self.unsupported("External stylesheets are not supported"))
     }
 }
diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml
index 4d012086..f087d814 100644
--- a/bindings/c/Cargo.toml
+++ b/bindings/c/Cargo.toml
@@ -18,4 +18,4 @@ cbindgen = "0.26"
 path = "../../css-inline"
 version = "*"
 default-features = false
-features = ["http", "file"]
+features = ["http-blocking", "file"]
diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs
index b40a5fde..490fbc60 100644
--- a/bindings/c/src/lib.rs
+++ b/bindings/c/src/lib.rs
@@ -1,4 +1,7 @@
-use css_inline::{CSSInliner, DefaultStylesheetResolver, InlineError, InlineOptions, Url};
+use css_inline::{
+    blocking::{CSSInliner, InlineOptions},
+    DefaultStylesheetResolver, InlineError, Url,
+};
 use libc::{c_char, size_t};
 use std::{borrow::Cow, cmp, ffi::CStr, io::Write, ptr, sync::Arc};
 
diff --git a/bindings/javascript/Cargo.toml b/bindings/javascript/Cargo.toml
index fbb0e945..fa109511 100644
--- a/bindings/javascript/Cargo.toml
+++ b/bindings/javascript/Cargo.toml
@@ -32,7 +32,7 @@ serde = { version = "1", features = ["derive"], default-features = false }
 path = "../../css-inline"
 version = "*"
 default-features = false
-features = ["http", "file"]
+features = ["http-blocking", "file"]
 
 [target.'cfg(target_arch = "wasm32")'.dependencies.css-inline]
 path = "../../css-inline"
diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs
index d96e95d8..987853ea 100644
--- a/bindings/javascript/src/lib.rs
+++ b/bindings/javascript/src/lib.rs
@@ -60,6 +60,6 @@ export function inline(html: string, options?: InlineOptions): string;
 export function version(): string;"#;
 
 fn inline_inner(html: String, options: Options) -> std::result::Result<String, errors::JsError> {
-    let inliner = css_inline::CSSInliner::new(options.try_into()?);
+    let inliner = css_inline::blocking::CSSInliner::new(options.try_into()?);
     Ok(inliner.inline(&html).map_err(errors::InlineError)?)
 }
diff --git a/bindings/javascript/src/options.rs b/bindings/javascript/src/options.rs
index 2691df49..a07c57a9 100644
--- a/bindings/javascript/src/options.rs
+++ b/bindings/javascript/src/options.rs
@@ -42,11 +42,11 @@ pub struct Options {
     pub preallocate_node_capacity: Option<u32>,
 }
 
-impl TryFrom<Options> for css_inline::InlineOptions<'_> {
+impl TryFrom<Options> for css_inline::blocking::InlineOptions<'_> {
     type Error = JsError;
 
     fn try_from(value: Options) -> std::result::Result<Self, Self::Error> {
-        Ok(css_inline::InlineOptions {
+        Ok(css_inline::blocking::InlineOptions {
             inline_style_tags: value.inline_style_tags.unwrap_or(true),
             keep_style_tags: value.keep_style_tags.unwrap_or(false),
             keep_link_tags: value.keep_link_tags.unwrap_or(false),
@@ -75,7 +75,7 @@ impl TryFrom<Options> for css_inline::InlineOptions<'_> {
                     pub struct UnsupportedResolver;
 
                     impl css_inline::StylesheetResolver for UnsupportedResolver {
-                        fn retrieve(&self, location: &str) -> css_inline::Result<String> {
+                        fn retrieve_blocking(&self, location: &str) -> css_inline::Result<String> {
                             let message = if location.starts_with("https")
                                 | location.starts_with("http")
                             {
diff --git a/bindings/javascript/wasm/index.js b/bindings/javascript/wasm/index.js
index a1d5a90c..5e3c0a2e 100644
--- a/bindings/javascript/wasm/index.js
+++ b/bindings/javascript/wasm/index.js
@@ -33,6 +33,18 @@ heap.push(void 0, null, true, false);
 function getObject(idx) {
   return heap[idx];
 }
+var heap_next = heap.length;
+function dropObject(idx) {
+  if (idx < 132)
+    return;
+  heap[idx] = heap_next;
+  heap_next = idx;
+}
+function takeObject(idx) {
+  const ret = getObject(idx);
+  dropObject(idx);
+  return ret;
+}
 var WASM_VECTOR_LEN = 0;
 var cachedUint8Memory0 = null;
 function getUint8Memory0() {
@@ -104,7 +116,6 @@ function getStringFromWasm0(ptr, len) {
   ptr = ptr >>> 0;
   return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
 }
-var heap_next = heap.length;
 function addHeapObject(obj) {
   if (heap_next === heap.length)
     heap.push(heap.length + 1);
@@ -113,17 +124,6 @@ function addHeapObject(obj) {
   heap[idx] = obj;
   return idx;
 }
-function dropObject(idx) {
-  if (idx < 132)
-    return;
-  heap[idx] = heap_next;
-  heap_next = idx;
-}
-function takeObject(idx) {
-  const ret = getObject(idx);
-  dropObject(idx);
-  return ret;
-}
 var cachedFloat64Memory0 = null;
 function getFloat64Memory0() {
   if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
@@ -261,6 +261,9 @@ function __wbg_get_imports() {
     const ret = getObject(arg0) === void 0;
     return ret;
   };
+  imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
+    takeObject(arg0);
+  };
   imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
     const obj = getObject(arg1);
     const ret = typeof obj === "string" ? obj : void 0;
@@ -303,9 +306,6 @@ function __wbg_get_imports() {
     const ret = +getObject(arg0);
     return ret;
   };
-  imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
-    takeObject(arg0);
-  };
   imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) {
     const ret = getObject(arg0).length;
     return ret;
diff --git a/bindings/javascript/wasm/index.min.js b/bindings/javascript/wasm/index.min.js
index eaf8dad1..a440b878 100644
--- a/bindings/javascript/wasm/index.min.js
+++ b/bindings/javascript/wasm/index.min.js
@@ -1,2 +1,2 @@
-"use strict";var cssInline=(()=>{var S=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var B=(e,n)=>{for(var t in n)S(e,t,{get:n[t],enumerable:!0})},q=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of D(n))!N.call(e,i)&&i!==t&&S(e,i,{get:()=>n[i],enumerable:!(r=$(n,i))||r.enumerable});return e};var z=e=>q(S({},"__esModule",{value:!0}),e);var Y={};B(Y,{initWasm:()=>K,inline:()=>Q,version:()=>X});var c,b=new Array(128).fill(void 0);b.push(void 0,null,!0,!1);function o(e){return b[e]}var h=0,g=null;function A(){return(g===null||g.byteLength===0)&&(g=new Uint8Array(c.memory.buffer)),g}var I=typeof TextEncoder<"u"?new TextEncoder("utf-8"):{encode:()=>{throw Error("TextEncoder not available")}},C=typeof I.encodeInto=="function"?function(e,n){return I.encodeInto(e,n)}:function(e,n){let t=I.encode(e);return n.set(t),{read:e.length,written:t.length}};function W(e,n,t){if(t===void 0){let _=I.encode(e),a=n(_.length,1)>>>0;return A().subarray(a,a+_.length).set(_),h=_.length,a}let r=e.length,i=n(r,1)>>>0,f=A(),s=0;for(;s<r;s++){let _=e.charCodeAt(s);if(_>127)break;f[i+s]=_}if(s!==r){s!==0&&(e=e.slice(s)),i=t(i,r,r=s+e.length*3,1)>>>0;let _=A().subarray(i+s,i+r),a=C(e,_);s+=a.written}return h=s,i}function j(e){return e==null}var w=null;function u(){return(w===null||w.byteLength===0)&&(w=new Int32Array(c.memory.buffer)),w}var M=typeof TextDecoder<"u"?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};typeof TextDecoder<"u"&&M.decode();function m(e,n){return e=e>>>0,M.decode(A().subarray(e,e+n))}var p=b.length;function l(e){p===b.length&&b.push(b.length+1);let n=p;return p=b[n],b[n]=e,n}function P(e){e<132||(b[e]=p,p=e)}function O(e){let n=o(e);return P(e),n}var y=null;function H(){return(y===null||y.byteLength===0)&&(y=new Float64Array(c.memory.buffer)),y}function E(e){let n=typeof e;if(n=="number"||n=="boolean"||e==null)return`${e}`;if(n=="string")return`"${e}"`;if(n=="symbol"){let i=e.description;return i==null?"Symbol":`Symbol(${i})`}if(n=="function"){let i=e.name;return typeof i=="string"&&i.length>0?`Function(${i})`:"Function"}if(Array.isArray(e)){let i=e.length,f="[";i>0&&(f+=E(e[0]));for(let s=1;s<i;s++)f+=", "+E(e[s]);return f+="]",f}let t=/\[object ([^\]]+)\]/.exec(toString.call(e)),r;if(t.length>1)r=t[1];else return toString.call(e);if(r=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message}
-${e.stack}`:r}function T(e,n){let t,r;try{let d=c.__wbindgen_add_to_stack_pointer(-16),L=W(e,c.__wbindgen_malloc,c.__wbindgen_realloc),R=h;c.inline(d,L,R,l(n));var i=u()[d/4+0],f=u()[d/4+1],s=u()[d/4+2],_=u()[d/4+3],a=i,x=f;if(_)throw a=0,x=0,O(s);return t=a,r=x,m(a,x)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(t,r,1)}}function k(){let e,n;try{let i=c.__wbindgen_add_to_stack_pointer(-16);c.version(i);var t=u()[i/4+0],r=u()[i/4+1];return e=t,n=r,m(t,r)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(e,n,1)}}async function J(e,n){if(typeof Response=="function"&&e instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(e,n)}catch(r){if(e.headers.get("Content-Type")!="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}let t=await e.arrayBuffer();return await WebAssembly.instantiate(t,n)}else{let t=await WebAssembly.instantiate(e,n);return t instanceof WebAssembly.Instance?{instance:t,module:e}:t}}function V(){let e={};return e.wbg={},e.wbg.__wbindgen_is_undefined=function(n){return o(n)===void 0},e.wbg.__wbindgen_string_get=function(n,t){let r=o(t),i=typeof r=="string"?r:void 0;var f=j(i)?0:W(i,c.__wbindgen_malloc,c.__wbindgen_realloc),s=h;u()[n/4+1]=s,u()[n/4+0]=f},e.wbg.__wbindgen_boolean_get=function(n){let t=o(n);return typeof t=="boolean"?t?1:0:2},e.wbg.__wbindgen_is_object=function(n){let t=o(n);return typeof t=="object"&&t!==null},e.wbg.__wbindgen_string_new=function(n,t){let r=m(n,t);return l(r)},e.wbg.__wbindgen_object_clone_ref=function(n){let t=o(n);return l(t)},e.wbg.__wbg_getwithrefkey_4a92a5eca60879b9=function(n,t){let r=o(n)[o(t)];return l(r)},e.wbg.__wbindgen_in=function(n,t){return o(n)in o(t)},e.wbg.__wbg_isSafeInteger_f93fde0dca9820f8=function(n){return Number.isSafeInteger(o(n))},e.wbg.__wbindgen_as_number=function(n){return+o(n)},e.wbg.__wbindgen_object_drop_ref=function(n){O(n)},e.wbg.__wbg_length_1d25fa9e4ac21ce7=function(n){return o(n).length},e.wbg.__wbindgen_memory=function(){let n=c.memory;return l(n)},e.wbg.__wbg_buffer_a448f833075b71ba=function(n){let t=o(n).buffer;return l(t)},e.wbg.__wbg_new_8f67e318f15d7254=function(n){let t=new Uint8Array(o(n));return l(t)},e.wbg.__wbg_set_2357bf09366ee480=function(n,t,r){o(n).set(o(t),r>>>0)},e.wbg.__wbindgen_error_new=function(n,t){let r=new Error(m(n,t));return l(r)},e.wbg.__wbindgen_jsval_loose_eq=function(n,t){return o(n)==o(t)},e.wbg.__wbindgen_number_get=function(n,t){let r=o(t),i=typeof r=="number"?r:void 0;H()[n/8+1]=j(i)?0:i,u()[n/4+0]=!j(i)},e.wbg.__wbg_instanceof_Uint8Array_bced6f43aed8c1aa=function(n){let t;try{t=o(n)instanceof Uint8Array}catch{t=!1}return t},e.wbg.__wbg_instanceof_ArrayBuffer_e7d53d51371448e2=function(n){let t;try{t=o(n)instanceof ArrayBuffer}catch{t=!1}return t},e.wbg.__wbindgen_debug_string=function(n,t){let r=E(o(t)),i=W(r,c.__wbindgen_malloc,c.__wbindgen_realloc),f=h;u()[n/4+1]=f,u()[n/4+0]=i},e.wbg.__wbindgen_throw=function(n,t){throw new Error(m(n,t))},e}function G(e,n){return c=e.exports,U.__wbindgen_wasm_module=n,y=null,w=null,g=null,c}async function U(e){if(c!==void 0)return c;typeof e>"u"&&(e=new URL("index_bg.wasm",void 0));let n=V();(typeof e=="string"||typeof Request=="function"&&e instanceof Request||typeof URL=="function"&&e instanceof URL)&&(e=fetch(e));let{instance:t,module:r}=await J(await e,n);return G(t,r)}var v=U;var F=!1,K=async e=>{if(F)throw new Error("Already initialized. The `initWasm()` function can be used only once.");await v(await e),F=!0};function Q(e,n){return T(e,n)}function X(){return k()}return z(Y);})();
+"use strict";var cssInline=(()=>{var S=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var B=(e,n)=>{for(var t in n)S(e,t,{get:n[t],enumerable:!0})},q=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of D(n))!N.call(e,i)&&i!==t&&S(e,i,{get:()=>n[i],enumerable:!(r=$(n,i))||r.enumerable});return e};var z=e=>q(S({},"__esModule",{value:!0}),e);var Y={};B(Y,{initWasm:()=>K,inline:()=>Q,version:()=>X});var c,b=new Array(128).fill(void 0);b.push(void 0,null,!0,!1);function o(e){return b[e]}var m=b.length;function C(e){e<132||(b[e]=m,m=e)}function M(e){let n=o(e);return C(e),n}var h=0,g=null;function A(){return(g===null||g.byteLength===0)&&(g=new Uint8Array(c.memory.buffer)),g}var I=typeof TextEncoder<"u"?new TextEncoder("utf-8"):{encode:()=>{throw Error("TextEncoder not available")}},P=typeof I.encodeInto=="function"?function(e,n){return I.encodeInto(e,n)}:function(e,n){let t=I.encode(e);return n.set(t),{read:e.length,written:t.length}};function W(e,n,t){if(t===void 0){let _=I.encode(e),a=n(_.length,1)>>>0;return A().subarray(a,a+_.length).set(_),h=_.length,a}let r=e.length,i=n(r,1)>>>0,f=A(),s=0;for(;s<r;s++){let _=e.charCodeAt(s);if(_>127)break;f[i+s]=_}if(s!==r){s!==0&&(e=e.slice(s)),i=t(i,r,r=s+e.length*3,1)>>>0;let _=A().subarray(i+s,i+r),a=P(e,_);s+=a.written}return h=s,i}function j(e){return e==null}var w=null;function u(){return(w===null||w.byteLength===0)&&(w=new Int32Array(c.memory.buffer)),w}var O=typeof TextDecoder<"u"?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};typeof TextDecoder<"u"&&O.decode();function p(e,n){return e=e>>>0,O.decode(A().subarray(e,e+n))}function l(e){m===b.length&&b.push(b.length+1);let n=m;return m=b[n],b[n]=e,n}var y=null;function H(){return(y===null||y.byteLength===0)&&(y=new Float64Array(c.memory.buffer)),y}function E(e){let n=typeof e;if(n=="number"||n=="boolean"||e==null)return`${e}`;if(n=="string")return`"${e}"`;if(n=="symbol"){let i=e.description;return i==null?"Symbol":`Symbol(${i})`}if(n=="function"){let i=e.name;return typeof i=="string"&&i.length>0?`Function(${i})`:"Function"}if(Array.isArray(e)){let i=e.length,f="[";i>0&&(f+=E(e[0]));for(let s=1;s<i;s++)f+=", "+E(e[s]);return f+="]",f}let t=/\[object ([^\]]+)\]/.exec(toString.call(e)),r;if(t.length>1)r=t[1];else return toString.call(e);if(r=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message}
+${e.stack}`:r}function T(e,n){let t,r;try{let d=c.__wbindgen_add_to_stack_pointer(-16),L=W(e,c.__wbindgen_malloc,c.__wbindgen_realloc),R=h;c.inline(d,L,R,l(n));var i=u()[d/4+0],f=u()[d/4+1],s=u()[d/4+2],_=u()[d/4+3],a=i,x=f;if(_)throw a=0,x=0,M(s);return t=a,r=x,p(a,x)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(t,r,1)}}function k(){let e,n;try{let i=c.__wbindgen_add_to_stack_pointer(-16);c.version(i);var t=u()[i/4+0],r=u()[i/4+1];return e=t,n=r,p(t,r)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(e,n,1)}}async function J(e,n){if(typeof Response=="function"&&e instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(e,n)}catch(r){if(e.headers.get("Content-Type")!="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}let t=await e.arrayBuffer();return await WebAssembly.instantiate(t,n)}else{let t=await WebAssembly.instantiate(e,n);return t instanceof WebAssembly.Instance?{instance:t,module:e}:t}}function V(){let e={};return e.wbg={},e.wbg.__wbindgen_is_undefined=function(n){return o(n)===void 0},e.wbg.__wbindgen_object_drop_ref=function(n){M(n)},e.wbg.__wbindgen_string_get=function(n,t){let r=o(t),i=typeof r=="string"?r:void 0;var f=j(i)?0:W(i,c.__wbindgen_malloc,c.__wbindgen_realloc),s=h;u()[n/4+1]=s,u()[n/4+0]=f},e.wbg.__wbindgen_boolean_get=function(n){let t=o(n);return typeof t=="boolean"?t?1:0:2},e.wbg.__wbindgen_is_object=function(n){let t=o(n);return typeof t=="object"&&t!==null},e.wbg.__wbindgen_string_new=function(n,t){let r=p(n,t);return l(r)},e.wbg.__wbindgen_object_clone_ref=function(n){let t=o(n);return l(t)},e.wbg.__wbg_getwithrefkey_4a92a5eca60879b9=function(n,t){let r=o(n)[o(t)];return l(r)},e.wbg.__wbindgen_in=function(n,t){return o(n)in o(t)},e.wbg.__wbg_isSafeInteger_f93fde0dca9820f8=function(n){return Number.isSafeInteger(o(n))},e.wbg.__wbindgen_as_number=function(n){return+o(n)},e.wbg.__wbg_length_1d25fa9e4ac21ce7=function(n){return o(n).length},e.wbg.__wbindgen_memory=function(){let n=c.memory;return l(n)},e.wbg.__wbg_buffer_a448f833075b71ba=function(n){let t=o(n).buffer;return l(t)},e.wbg.__wbg_new_8f67e318f15d7254=function(n){let t=new Uint8Array(o(n));return l(t)},e.wbg.__wbg_set_2357bf09366ee480=function(n,t,r){o(n).set(o(t),r>>>0)},e.wbg.__wbindgen_error_new=function(n,t){let r=new Error(p(n,t));return l(r)},e.wbg.__wbindgen_jsval_loose_eq=function(n,t){return o(n)==o(t)},e.wbg.__wbindgen_number_get=function(n,t){let r=o(t),i=typeof r=="number"?r:void 0;H()[n/8+1]=j(i)?0:i,u()[n/4+0]=!j(i)},e.wbg.__wbg_instanceof_Uint8Array_bced6f43aed8c1aa=function(n){let t;try{t=o(n)instanceof Uint8Array}catch{t=!1}return t},e.wbg.__wbg_instanceof_ArrayBuffer_e7d53d51371448e2=function(n){let t;try{t=o(n)instanceof ArrayBuffer}catch{t=!1}return t},e.wbg.__wbindgen_debug_string=function(n,t){let r=E(o(t)),i=W(r,c.__wbindgen_malloc,c.__wbindgen_realloc),f=h;u()[n/4+1]=f,u()[n/4+0]=i},e.wbg.__wbindgen_throw=function(n,t){throw new Error(p(n,t))},e}function G(e,n){return c=e.exports,U.__wbindgen_wasm_module=n,y=null,w=null,g=null,c}async function U(e){if(c!==void 0)return c;typeof e>"u"&&(e=new URL("index_bg.wasm",void 0));let n=V();(typeof e=="string"||typeof Request=="function"&&e instanceof Request||typeof URL=="function"&&e instanceof URL)&&(e=fetch(e));let{instance:t,module:r}=await J(await e,n);return G(t,r)}var v=U;var F=!1,K=async e=>{if(F)throw new Error("Already initialized. The `initWasm()` function can be used only once.");await v(await e),F=!0};function Q(e,n){return T(e,n)}function X(){return k()}return z(Y);})();
diff --git a/bindings/javascript/wasm/index.mjs b/bindings/javascript/wasm/index.mjs
index 008fbb26..6ec93e90 100644
--- a/bindings/javascript/wasm/index.mjs
+++ b/bindings/javascript/wasm/index.mjs
@@ -5,6 +5,18 @@ heap.push(void 0, null, true, false);
 function getObject(idx) {
   return heap[idx];
 }
+var heap_next = heap.length;
+function dropObject(idx) {
+  if (idx < 132)
+    return;
+  heap[idx] = heap_next;
+  heap_next = idx;
+}
+function takeObject(idx) {
+  const ret = getObject(idx);
+  dropObject(idx);
+  return ret;
+}
 var WASM_VECTOR_LEN = 0;
 var cachedUint8Memory0 = null;
 function getUint8Memory0() {
@@ -76,7 +88,6 @@ function getStringFromWasm0(ptr, len) {
   ptr = ptr >>> 0;
   return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
 }
-var heap_next = heap.length;
 function addHeapObject(obj) {
   if (heap_next === heap.length)
     heap.push(heap.length + 1);
@@ -85,17 +96,6 @@ function addHeapObject(obj) {
   heap[idx] = obj;
   return idx;
 }
-function dropObject(idx) {
-  if (idx < 132)
-    return;
-  heap[idx] = heap_next;
-  heap_next = idx;
-}
-function takeObject(idx) {
-  const ret = getObject(idx);
-  dropObject(idx);
-  return ret;
-}
 var cachedFloat64Memory0 = null;
 function getFloat64Memory0() {
   if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
@@ -233,6 +233,9 @@ function __wbg_get_imports() {
     const ret = getObject(arg0) === void 0;
     return ret;
   };
+  imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
+    takeObject(arg0);
+  };
   imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
     const obj = getObject(arg1);
     const ret = typeof obj === "string" ? obj : void 0;
@@ -275,9 +278,6 @@ function __wbg_get_imports() {
     const ret = +getObject(arg0);
     return ret;
   };
-  imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
-    takeObject(arg0);
-  };
   imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) {
     const ret = getObject(arg0).length;
     return ret;
diff --git a/bindings/javascript/wasm/index_bg.wasm b/bindings/javascript/wasm/index_bg.wasm
index a61fbf8e..8fb999b9 100644
Binary files a/bindings/javascript/wasm/index_bg.wasm and b/bindings/javascript/wasm/index_bg.wasm differ
diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml
index d64ceafc..98c6131e 100644
--- a/bindings/python/Cargo.toml
+++ b/bindings/python/Cargo.toml
@@ -23,7 +23,7 @@ url = "2"
 path = "../../css-inline"
 version = "*"
 default-features = false
-features = ["http", "file"]
+features = ["http-blocking", "file"]
 
 [profile.release]
 codegen-units = 1
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index b2a86651..d7a232dd 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -74,12 +74,12 @@ fn parse_url(url: Option<String>) -> PyResult<Option<rust_inline::Url>> {
 /// Customizable CSS inliner.
 #[pyclass]
 struct CSSInliner {
-    inner: rust_inline::CSSInliner<'static>,
+    inner: rust_inline::blocking::CSSInliner<'static>,
 }
 
 macro_rules! inliner {
     ($inline_style_tags:expr, $keep_style_tags:expr, $keep_link_tags:expr, $base_url:expr, $load_remote_stylesheets:expr, $extra_css:expr, $preallocate_node_capacity:expr) => {{
-        let options = rust_inline::InlineOptions {
+        rust_inline::blocking::InlineOptions {
             inline_style_tags: $inline_style_tags.unwrap_or(true),
             keep_style_tags: $keep_style_tags.unwrap_or(false),
             keep_link_tags: $keep_link_tags.unwrap_or(false),
@@ -88,8 +88,8 @@ macro_rules! inliner {
             extra_css: $extra_css.map(Into::into),
             preallocate_node_capacity: $preallocate_node_capacity.unwrap_or(32),
             resolver: std::sync::Arc::new(rust_inline::DefaultStylesheetResolver),
-        };
-        rust_inline::CSSInliner::new(options)
+        }
+        .build()
     }};
 }
 
@@ -198,7 +198,7 @@ fn inline_many(
 }
 
 fn inline_many_impl(
-    inliner: &rust_inline::CSSInliner<'_>,
+    inliner: &rust_inline::blocking::CSSInliner<'_>,
     htmls: &PyList,
 ) -> PyResult<Vec<String>> {
     // Extract strings from the list. It will fail if there is any non-string value
diff --git a/bindings/ruby/ext/css_inline/Cargo.toml b/bindings/ruby/ext/css_inline/Cargo.toml
index a71fb898..97fc8e02 100644
--- a/bindings/ruby/ext/css_inline/Cargo.toml
+++ b/bindings/ruby/ext/css_inline/Cargo.toml
@@ -23,4 +23,4 @@ rayon = "1"
 path = "../../../../css-inline"
 version = "*"
 default-features = false
-features = ["http", "file"]
+features = ["http-blocking", "file"]
diff --git a/bindings/ruby/ext/css_inline/src/lib.rs b/bindings/ruby/ext/css_inline/src/lib.rs
index f5395eba..e2ced6c9 100644
--- a/bindings/ruby/ext/css_inline/src/lib.rs
+++ b/bindings/ruby/ext/css_inline/src/lib.rs
@@ -39,7 +39,7 @@ type RubyResult<T> = Result<T, magnus::Error>;
 
 fn parse_options<Req>(
     args: &Args<Req, (), (), (), RHash, ()>,
-) -> RubyResult<rust_inline::InlineOptions<'static>> {
+) -> RubyResult<rust_inline::blocking::InlineOptions<'static>> {
     let kwargs = get_kwargs::<
         _,
         (),
@@ -67,7 +67,7 @@ fn parse_options<Req>(
         ],
     )?;
     let kwargs = kwargs.optional;
-    Ok(rust_inline::InlineOptions {
+    Ok(rust_inline::blocking::InlineOptions {
         inline_style_tags: kwargs.0.unwrap_or(true),
         keep_style_tags: kwargs.1.unwrap_or(false),
         keep_link_tags: kwargs.2.unwrap_or(false),
@@ -81,7 +81,7 @@ fn parse_options<Req>(
 
 #[magnus::wrap(class = "CSSInline::CSSInliner")]
 struct CSSInliner {
-    inner: rust_inline::CSSInliner<'static>,
+    inner: rust_inline::blocking::CSSInliner<'static>,
 }
 
 struct InlineErrorWrapper(rust_inline::InlineError);
@@ -133,7 +133,7 @@ impl CSSInliner {
         let args = scan_args::<(), _, _, _, _, _>(args)?;
         let options = parse_options(&args)?;
         Ok(CSSInliner {
-            inner: rust_inline::CSSInliner::new(options),
+            inner: rust_inline::blocking::CSSInliner::new(options),
         })
     }
 
@@ -152,7 +152,7 @@ fn inline(args: &[Value]) -> RubyResult<String> {
     let args = scan_args::<(String,), _, _, _, _, _>(args)?;
     let options = parse_options(&args)?;
     let html = args.required.0;
-    Ok(rust_inline::CSSInliner::new(options)
+    Ok(rust_inline::blocking::CSSInliner::new(options)
         .inline(&html)
         .map_err(InlineErrorWrapper)?)
 }
@@ -160,13 +160,13 @@ fn inline(args: &[Value]) -> RubyResult<String> {
 fn inline_many(args: &[Value]) -> RubyResult<Vec<String>> {
     let args = scan_args::<(Vec<String>,), _, _, _, _, _>(args)?;
     let options = parse_options(&args)?;
-    let inliner = rust_inline::CSSInliner::new(options);
+    let inliner = rust_inline::blocking::CSSInliner::new(options);
     inline_many_impl(&args.required.0, &inliner)
 }
 
 fn inline_many_impl(
     htmls: &[String],
-    inliner: &rust_inline::CSSInliner<'static>,
+    inliner: &rust_inline::blocking::CSSInliner<'static>,
 ) -> RubyResult<Vec<String>> {
     let output: Result<Vec<_>, _> = htmls.par_iter().map(|html| inliner.inline(html)).collect();
     Ok(output.map_err(InlineErrorWrapper)?)
diff --git a/css-inline/Cargo.toml b/css-inline/Cargo.toml
index 22cae200..a4e29a2e 100644
--- a/css-inline/Cargo.toml
+++ b/css-inline/Cargo.toml
@@ -22,18 +22,20 @@ rust-version = "1.65"
 name = "css-inline"
 
 [features]
-default = ["cli", "http", "file"]
+default = ["cli", "http", "http-blocking", "file"]
 cli = ["pico-args", "rayon"]
 http = ["reqwest"]
+http-blocking = ["reqwest/blocking"]
 file = []
 
 [dependencies]
 cssparser = "0.31.2"
+futures-util = "0.3.30"
 html5ever = "0.26.0"
 indexmap = "2.1"
 pico-args = { version = "0.3", optional = true }
 rayon = { version = "1.7", optional = true }
-reqwest = { version = "0.11.23", optional = true, default-features = false, features = ["rustls-tls", "blocking"] }
+reqwest = { version = "0.11.23", optional = true, default-features = false, features = ["rustls-tls"] }
 rustc-hash = "1.1.0"
 selectors = "0.25.0"
 smallvec = "1"
@@ -46,6 +48,7 @@ criterion = { version = "0.5.1", features = [], default-features = false }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 test-case = "3.3"
+tokio = { version = "1.32", features = ["macros", "rt"] }
 
 [[bench]]
 name = "inliner"
diff --git a/css-inline/benches/inliner.rs b/css-inline/benches/inliner.rs
index e009b328..76ab8c3f 100644
--- a/css-inline/benches/inliner.rs
+++ b/css-inline/benches/inliner.rs
@@ -1,5 +1,5 @@
 use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion};
-use css_inline::inline_to;
+use css_inline::blocking::inline_to;
 use std::fs;
 
 #[derive(serde::Deserialize, Debug)]
diff --git a/css-inline/src/error.rs b/css-inline/src/error.rs
index 5f21deb3..1d7ce97c 100644
--- a/css-inline/src/error.rs
+++ b/css-inline/src/error.rs
@@ -20,7 +20,7 @@ pub enum InlineError {
     /// from the filesystem.
     IO(io::Error),
     /// Network-related problem. E.g. resource is not available.
-    #[cfg(feature = "http")]
+    #[cfg(any(feature = "http", feature = "http-blocking"))]
     Network {
         /// Original network error.
         error: reqwest::Error,
@@ -41,7 +41,7 @@ impl Error for InlineError {
     fn source(&self) -> Option<&(dyn Error + 'static)> {
         match self {
             InlineError::IO(error) => Some(error),
-            #[cfg(feature = "http")]
+            #[cfg(any(feature = "http", feature = "http-blocking"))]
             InlineError::Network { error, .. } => Some(error),
             InlineError::MissingStyleSheet { .. } | InlineError::ParseError(_) => None,
         }
@@ -52,7 +52,7 @@ impl Display for InlineError {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         match self {
             Self::IO(error) => error.fmt(f),
-            #[cfg(feature = "http")]
+            #[cfg(any(feature = "http", feature = "http-blocking"))]
             Self::Network { error, location } => f.write_fmt(format_args!("{error}: {location}")),
             Self::ParseError(error) => f.write_str(error),
             Self::MissingStyleSheet { path } => {
diff --git a/css-inline/src/lib.rs b/css-inline/src/lib.rs
index 04aeeb63..56d4d253 100644
--- a/css-inline/src/lib.rs
+++ b/css-inline/src/lib.rs
@@ -34,8 +34,7 @@ mod parser;
 mod resolver;
 
 pub use error::InlineError;
-use indexmap::IndexMap;
-use std::{borrow::Cow, fmt::Formatter, hash::BuildHasherDefault, io::Write, sync::Arc};
+use std::{hash::BuildHasherDefault, io::Write};
 
 use crate::html::ElementStyleMap;
 use hasher::BuildNoHashHasher;
@@ -43,214 +42,32 @@ use html::Document;
 pub use resolver::{DefaultStylesheetResolver, StylesheetResolver};
 pub use url::{ParseError, Url};
 
-/// Configuration options for CSS inlining process.
-#[allow(clippy::struct_excessive_bools)]
-pub struct InlineOptions<'a> {
-    /// Whether to inline CSS from "style" tags.
-    ///
-    /// Sometimes HTML may include a lot of boilerplate styles, that are not applicable in every
-    /// scenario and it is useful to ignore them and use `extra_css` instead.
-    pub inline_style_tags: bool,
-    /// Keep "style" tags after inlining.
-    pub keep_style_tags: bool,
-    /// Keep "link" tags after inlining.
-    pub keep_link_tags: bool,
-    /// Used for loading external stylesheets via relative URLs.
-    pub base_url: Option<Url>,
-    /// Whether remote stylesheets should be loaded or not.
-    pub load_remote_stylesheets: bool,
-    // The point of using `Cow` here is Python bindings, where it is problematic to pass a reference
-    // without dealing with memory leaks & unsafe. With `Cow` we can use moved values as `String` in
-    // Python wrapper for `CSSInliner` and `&str` in Rust & simple functions on the Python side
-    /// Additional CSS to inline.
-    pub extra_css: Option<Cow<'a, str>>,
-    /// Pre-allocate capacity for HTML nodes during parsing.
-    /// It can improve performance when you have an estimate of the number of nodes in your HTML document.
-    pub preallocate_node_capacity: usize,
-    /// A way to resolve stylesheets from various sources.
-    pub resolver: Arc<dyn StylesheetResolver>,
-}
-
-impl<'a> std::fmt::Debug for InlineOptions<'a> {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("InlineOptions")
-            .field("inline_style_tags", &self.inline_style_tags)
-            .field("keep_style_tags", &self.keep_style_tags)
-            .field("keep_link_tags", &self.keep_link_tags)
-            .field("base_url", &self.base_url)
-            .field("load_remote_stylesheets", &self.load_remote_stylesheets)
-            .field("extra_css", &self.extra_css)
-            .field("preallocate_node_capacity", &self.preallocate_node_capacity)
-            .finish_non_exhaustive()
-    }
-}
-
-impl<'a> InlineOptions<'a> {
-    /// Override whether "style" tags should be inlined.
-    #[must_use]
-    pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
-        self.inline_style_tags = inline_style_tags;
-        self
-    }
-
-    /// Override whether "style" tags should be kept after processing.
-    #[must_use]
-    pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
-        self.keep_style_tags = keep_style_tags;
-        self
-    }
-
-    /// Override whether "link" tags should be kept after processing.
-    #[must_use]
-    pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
-        self.keep_link_tags = keep_link_tags;
-        self
-    }
-
-    /// Set base URL that will be used for loading external stylesheets via relative URLs.
-    #[must_use]
-    pub fn base_url(mut self, base_url: Option<Url>) -> Self {
-        self.base_url = base_url;
-        self
-    }
-
-    /// Override whether remote stylesheets should be loaded.
-    #[must_use]
-    pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
-        self.load_remote_stylesheets = load_remote_stylesheets;
-        self
-    }
-
-    /// Set additional CSS to inline.
-    #[must_use]
-    pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
-        self.extra_css = extra_css;
-        self
-    }
-
-    /// Set the initial node capacity for HTML tree.
-    #[must_use]
-    pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
-        self.preallocate_node_capacity = preallocate_node_capacity;
-        self
-    }
-
-    /// Set the way to resolve stylesheets from various sources.
-    #[must_use]
-    pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
-        self.resolver = resolver;
-        self
-    }
-
-    /// Create a new `CSSInliner` instance from this options.
-    #[must_use]
-    pub const fn build(self) -> CSSInliner<'a> {
-        CSSInliner::new(self)
-    }
-}
-
-impl Default for InlineOptions<'_> {
-    #[inline]
-    fn default() -> Self {
-        InlineOptions {
-            inline_style_tags: true,
-            keep_style_tags: false,
-            keep_link_tags: false,
-            base_url: None,
-            load_remote_stylesheets: true,
-            extra_css: None,
-            preallocate_node_capacity: 32,
-            resolver: Arc::new(DefaultStylesheetResolver),
-        }
-    }
-}
-
 /// A specialized `Result` type for CSS inlining operations.
 pub type Result<T> = std::result::Result<T, InlineError>;
 
-/// Customizable CSS inliner.
-#[derive(Debug)]
-pub struct CSSInliner<'a> {
-    options: InlineOptions<'a>,
-}
-
 const GROWTH_COEFFICIENT: f64 = 1.5;
 // A rough coefficient to calculate the number of individual declarations based on the total CSS size.
 const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
 
-impl<'a> CSSInliner<'a> {
-    /// Create a new `CSSInliner` instance with given options.
-    #[must_use]
-    #[inline]
-    pub const fn new(options: InlineOptions<'a>) -> Self {
-        CSSInliner { options }
-    }
-
-    /// Return a default `InlineOptions` that can fully configure the CSS inliner.
-    ///
-    /// # Examples
-    ///
-    /// Get default `InlineOptions`, then change base url
-    ///
-    /// ```rust
-    /// use css_inline::{CSSInliner, Url};
-    /// # use url::ParseError;
-    /// # fn run() -> Result<(), ParseError> {
-    /// let url = Url::parse("https://api.example.com")?;
-    /// let inliner = CSSInliner::options()
-    ///     .base_url(Some(url))
-    ///     .build();
-    /// # Ok(())
-    /// # }
-    /// # run().unwrap();
-    /// ```
-    #[must_use]
-    #[inline]
-    pub fn options() -> InlineOptions<'a> {
-        InlineOptions::default()
-    }
-
-    /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a
-    /// string.
-    ///
-    /// # Errors
-    ///
-    /// Inlining might fail for the following reasons:
-    ///   - Missing stylesheet file;
-    ///   - Remote stylesheet is not available;
-    ///   - IO errors;
-    ///   - Internal CSS selector parsing error;
-    #[inline]
-    pub fn inline(&self, html: &str) -> Result<String> {
-        // Allocating more memory than the input HTML, as the inlined version is usually bigger
-        #[allow(
-            clippy::cast_precision_loss,
-            clippy::cast_sign_loss,
-            clippy::cast_possible_truncation
-        )]
-        let mut out = Vec::with_capacity(
-            (html.len() as f64 * GROWTH_COEFFICIENT)
-                .min(usize::MAX as f64)
-                .round() as usize,
-        );
-        self.inline_to(html, &mut out)?;
-        Ok(String::from_utf8_lossy(&out).to_string())
-    }
+#[inline]
+fn build_output_buffer(input_length: usize) -> Vec<u8> {
+    // Allocating more memory than the input HTML, as the inlined version is usually bigger
+    #[allow(
+        clippy::cast_precision_loss,
+        clippy::cast_sign_loss,
+        clippy::cast_possible_truncation
+    )]
+    Vec::with_capacity(
+        (input_length as f64 * GROWTH_COEFFICIENT)
+            .min(usize::MAX as f64)
+            .round() as usize,
+    )
+}
 
-    /// Inline CSS & write the result to a generic writer. Use it if you want to write
-    /// the inlined document to a file.
-    ///
-    /// # Errors
-    ///
-    /// Inlining might fail for the following reasons:
-    ///   - Missing stylesheet file;
-    ///   - Remote stylesheet is not available;
-    ///   - IO errors;
-    ///   - Internal CSS selector parsing error;
-    #[inline]
-    pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
+macro_rules! inline_to_impl {
+    ($self:expr, $html:expr, $target:expr, $retrieve:expr, $($_await:tt)*) => {{
         let document =
-            Document::parse_with_options(html.as_bytes(), self.options.preallocate_node_capacity);
+            $crate::Document::parse_with_options($html.as_bytes(), $self.options.preallocate_node_capacity);
         // CSS rules may overlap, and the final set of rules applied to an element depend on
         // selectors' specificity - selectors with higher specificity have more priority.
         // Inlining happens in two major steps:
@@ -258,7 +75,7 @@ impl<'a> CSSInliner<'a> {
         //      selector's specificity. When two rules overlap on the same declaration, then
         //      the one with higher specificity replaces another.
         //   2. Resulting styles are merged into existing "style" tags.
-        let mut size_estimate: usize = if self.options.inline_style_tags {
+        let mut size_estimate: usize = if $self.options.inline_style_tags {
             document
                 .styles()
                 .map(|s| {
@@ -269,31 +86,32 @@ impl<'a> CSSInliner<'a> {
         } else {
             0
         };
-        if let Some(extra_css) = &self.options.extra_css {
+        if let Some(extra_css) = &$self.options.extra_css {
             size_estimate = size_estimate.saturating_add(extra_css.len());
         }
         let mut raw_styles = String::with_capacity(size_estimate);
-        if self.options.inline_style_tags {
+        if $self.options.inline_style_tags {
             for style in document.styles() {
                 raw_styles.push_str(style);
                 raw_styles.push('\n');
             }
         }
-        if self.options.load_remote_stylesheets {
+        if $self.options.load_remote_stylesheets {
             let mut links = document.stylesheets().collect::<Vec<&str>>();
             links.sort_unstable();
             links.dedup();
             for href in &links {
-                let url = self.get_full_url(href);
-                let css = self.options.resolver.retrieve(url.as_ref())?;
-                raw_styles.push_str(&css);
+                let url = $self.get_full_url(href);
+                #[allow(clippy::redundant_closure_call)]
+                let css = $retrieve(url.as_ref())$($_await)*;
+                raw_styles.push_str(css.as_str());
                 raw_styles.push('\n');
             }
         }
-        if let Some(extra_css) = &self.options.extra_css {
+        if let Some(extra_css) = &$self.options.extra_css {
             raw_styles.push_str(extra_css);
         }
-        let mut styles = IndexMap::with_capacity_and_hasher(128, BuildNoHashHasher::default());
+        let mut styles = indexmap::IndexMap::with_capacity_and_hasher(128, $crate::BuildNoHashHasher::default());
         let mut parse_input = cssparser::ParserInput::new(&raw_styles);
         let mut parser = cssparser::Parser::new(&mut parse_input);
         // Allocating some memory for all the parsed declarations
@@ -303,7 +121,7 @@ impl<'a> CSSInliner<'a> {
             clippy::cast_possible_truncation
         )]
         let mut declarations = Vec::with_capacity(
-            ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
+            ((raw_styles.len() as f64 / $crate::DECLARATION_SIZE_COEFFICIENT)
                 .min(usize::MAX as f64)
                 .round() as usize)
                 .max(16),
@@ -311,7 +129,7 @@ impl<'a> CSSInliner<'a> {
         let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
         for rule in cssparser::StyleSheetParser::new(
             &mut parser,
-            &mut parser::CSSRuleListParser::new(&mut declarations),
+            &mut $crate::parser::CSSRuleListParser::new(&mut declarations),
         )
         .flatten()
         {
@@ -327,9 +145,9 @@ impl<'a> CSSInliner<'a> {
                     for matching_element in matching_elements {
                         let element_styles =
                             styles.entry(matching_element.node_id).or_insert_with(|| {
-                                ElementStyleMap::with_capacity_and_hasher(
+                                $crate::ElementStyleMap::with_capacity_and_hasher(
                                     end.saturating_sub(*start).saturating_add(4),
-                                    BuildHasherDefault::default(),
+                                    $crate::BuildHasherDefault::default(),
                                 )
                             });
                         // Iterate over pairs of property name & value
@@ -353,38 +171,242 @@ impl<'a> CSSInliner<'a> {
             }
         }
         document.serialize(
-            target,
+            $target,
             styles,
-            self.options.keep_style_tags,
-            self.options.keep_link_tags,
+            $self.options.keep_style_tags,
+            $self.options.keep_link_tags,
         )?;
         Ok(())
-    }
+    }};
+}
 
-    fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
-        // Valid absolute URL
-        if Url::parse(href).is_ok() {
-            return Cow::Borrowed(href);
-        };
-        if let Some(base_url) = &self.options.base_url {
-            // Use the same scheme as the base URL
-            if href.starts_with("//") {
-                return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
+macro_rules! inliner_impl {
+    () => {
+        /// Configuration options for CSS inlining process.
+        #[allow(clippy::struct_excessive_bools)]
+        pub struct InlineOptions<'a> {
+            /// Whether to inline CSS from "style" tags.
+            ///
+            /// Sometimes HTML may include a lot of boilerplate styles, that are not applicable in every
+            /// scenario and it is useful to ignore them and use `extra_css` instead.
+            pub inline_style_tags: bool,
+            /// Keep "style" tags after inlining.
+            pub keep_style_tags: bool,
+            /// Keep "link" tags after inlining.
+            pub keep_link_tags: bool,
+            /// Used for loading external stylesheets via relative URLs.
+            #[allow(unused_qualifications)]
+            pub base_url: Option<url::Url>,
+            /// Whether remote stylesheets should be loaded or not.
+            pub load_remote_stylesheets: bool,
+            // The point of using `Cow` here is Python bindings, where it is problematic to pass a reference
+            // without dealing with memory leaks & unsafe. With `Cow` we can use moved values as `String` in
+            // Python wrapper for `CSSInliner` and `&str` in Rust & simple functions on the Python side
+            /// Additional CSS to inline.
+            pub extra_css: Option<std::borrow::Cow<'a, str>>,
+            /// Pre-allocate capacity for HTML nodes during parsing.
+            /// It can improve performance when you have an estimate of the number of nodes in your HTML document.
+            pub preallocate_node_capacity: usize,
+            /// A way to resolve stylesheets from various sources.
+            pub resolver: std::sync::Arc<dyn $crate::resolver::StylesheetResolver>,
+        }
+
+        impl<'a> std::fmt::Debug for InlineOptions<'a> {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                f.debug_struct("InlineOptions")
+                    .field("inline_style_tags", &self.inline_style_tags)
+                    .field("keep_style_tags", &self.keep_style_tags)
+                    .field("keep_link_tags", &self.keep_link_tags)
+                    .field("base_url", &self.base_url)
+                    .field("load_remote_stylesheets", &self.load_remote_stylesheets)
+                    .field("extra_css", &self.extra_css)
+                    .field("preallocate_node_capacity", &self.preallocate_node_capacity)
+                    .finish_non_exhaustive()
+            }
+        }
+
+        impl<'a> InlineOptions<'a> {
+            /// Override whether "style" tags should be inlined.
+            #[must_use]
+            pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
+                self.inline_style_tags = inline_style_tags;
+                self
             }
-            // Not a URL, then it is a relative URL
-            if let Ok(new_url) = base_url.join(href) {
-                return Cow::Owned(new_url.into());
+
+            /// Override whether "style" tags should be kept after processing.
+            #[must_use]
+            pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
+                self.keep_style_tags = keep_style_tags;
+                self
             }
-        };
-        // If it is not a valid URL and there is no base URL specified, we assume a local path
-        Cow::Borrowed(href)
-    }
+
+            /// Override whether "link" tags should be kept after processing.
+            #[must_use]
+            pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
+                self.keep_link_tags = keep_link_tags;
+                self
+            }
+
+            /// Set base URL that will be used for loading external stylesheets via relative URLs.
+            #[must_use]
+            #[allow(unused_qualifications)]
+            pub fn base_url(mut self, base_url: Option<url::Url>) -> Self {
+                self.base_url = base_url;
+                self
+            }
+
+            /// Override whether remote stylesheets should be loaded.
+            #[must_use]
+            pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
+                self.load_remote_stylesheets = load_remote_stylesheets;
+                self
+            }
+
+            /// Set additional CSS to inline.
+            #[must_use]
+            pub fn extra_css(mut self, extra_css: Option<std::borrow::Cow<'a, str>>) -> Self {
+                self.extra_css = extra_css;
+                self
+            }
+
+            /// Set the initial node capacity for HTML tree.
+            #[must_use]
+            pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
+                self.preallocate_node_capacity = preallocate_node_capacity;
+                self
+            }
+
+            /// Set the way to resolve stylesheets from various sources.
+            #[must_use]
+            pub fn resolver(
+                mut self,
+                resolver: std::sync::Arc<dyn $crate::resolver::StylesheetResolver>,
+            ) -> Self {
+                self.resolver = resolver;
+                self
+            }
+
+            /// Create a new `CSSInliner` instance from this options.
+            #[must_use]
+            pub const fn build(self) -> CSSInliner<'a> {
+                CSSInliner::new(self)
+            }
+        }
+
+        impl Default for InlineOptions<'_> {
+            #[inline]
+            fn default() -> Self {
+                InlineOptions {
+                    inline_style_tags: true,
+                    keep_style_tags: false,
+                    keep_link_tags: false,
+                    base_url: None,
+                    load_remote_stylesheets: true,
+                    extra_css: None,
+                    preallocate_node_capacity: 32,
+                    resolver: std::sync::Arc::new($crate::DefaultStylesheetResolver),
+                }
+            }
+        }
+        /// Customizable CSS inliner.
+        #[derive(Debug)]
+        pub struct CSSInliner<'a> {
+            options: InlineOptions<'a>,
+        }
+
+        impl<'a> CSSInliner<'a> {
+            /// Create a new `CSSInliner` instance with given options.
+            #[must_use]
+            #[inline]
+            pub const fn new(options: InlineOptions<'a>) -> Self {
+                CSSInliner { options }
+            }
+
+            /// Return a default `InlineOptions` that can fully configure the CSS inliner.
+            ///
+            /// # Examples
+            ///
+            /// Get default `InlineOptions`, then change base url
+            ///
+            /// ```rust
+            /// use css_inline::{CSSInliner, Url};
+            /// # use url::ParseError;
+            /// # fn run() -> Result<(), ParseError> {
+            /// let url = Url::parse("https://api.example.com")?;
+            /// let inliner = CSSInliner::options()
+            ///     .base_url(Some(url))
+            ///     .build();
+            /// # Ok(())
+            /// # }
+            /// # run().unwrap();
+            /// ```
+            #[must_use]
+            #[inline]
+            pub fn options() -> InlineOptions<'a> {
+                InlineOptions::default()
+            }
+
+            fn get_full_url<'u>(&self, href: &'u str) -> std::borrow::Cow<'u, str> {
+                // Valid absolute URL
+                if url::Url::parse(href).is_ok() {
+                    return std::borrow::Cow::Borrowed(href);
+                };
+                if let Some(base_url) = &self.options.base_url {
+                    // Use the same scheme as the base URL
+                    if href.starts_with("//") {
+                        return std::borrow::Cow::Owned(format!("{}:{}", base_url.scheme(), href));
+                    }
+                    // Not a URL, then it is a relative URL
+                    if let Ok(new_url) = base_url.join(href) {
+                        return std::borrow::Cow::Owned(new_url.into());
+                    }
+                };
+                // If it is not a valid URL and there is no base URL specified, we assume a local path
+                std::borrow::Cow::Borrowed(href)
+            }
+        }
+
+        impl Default for CSSInliner<'_> {
+            #[inline]
+            fn default() -> Self {
+                CSSInliner::new(InlineOptions::default())
+            }
+        }
+    };
 }
 
-impl Default for CSSInliner<'_> {
+inliner_impl!();
+
+impl<'a> CSSInliner<'a> {
+    /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a
+    /// string.
+    ///
+    /// # Errors
+    ///
+    /// Inlining might fail for the following reasons:
+    ///   - Missing stylesheet file;
+    ///   - Remote stylesheet is not available;
+    ///   - IO errors;
+    ///   - Internal CSS selector parsing error;
     #[inline]
-    fn default() -> Self {
-        CSSInliner::new(InlineOptions::default())
+    pub async fn inline(&self, html: &str) -> Result<String> {
+        let mut out = build_output_buffer(html.len());
+        self.inline_to(html, &mut out).await?;
+        Ok(String::from_utf8_lossy(&out).to_string())
+    }
+    /// Inline CSS & write the result to a generic writer. Use it if you want to write
+    /// the inlined document to a file.
+    ///
+    /// # Errors
+    ///
+    /// Inlining might fail for the following reasons:
+    ///   - Missing stylesheet file;
+    ///   - Remote stylesheet is not available;
+    ///   - IO errors;
+    ///   - Internal CSS selector parsing error;
+    #[inline]
+    pub async fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
+        inline_to_impl!(self, html, target, |location| async move { self.options.resolver.retrieve(location).await}, .await?)
     }
 }
 
@@ -398,8 +420,8 @@ impl Default for CSSInliner<'_> {
 ///   - IO errors;
 ///   - Internal CSS selector parsing error;
 #[inline]
-pub fn inline(html: &str) -> Result<String> {
-    CSSInliner::default().inline(html)
+pub async fn inline(html: &str) -> Result<String> {
+    CSSInliner::default().inline(html).await
 }
 
 /// Shortcut for inlining CSS with default parameters and writing the output to a generic writer.
@@ -412,6 +434,74 @@ pub fn inline(html: &str) -> Result<String> {
 ///   - IO errors;
 ///   - Internal CSS selector parsing error;
 #[inline]
-pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
-    CSSInliner::default().inline_to(html, target)
+pub async fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
+    CSSInliner::default().inline_to(html, target).await
+}
+
+/// Blocking API for CSS inlining.
+pub mod blocking {
+    use super::{build_output_buffer, Result};
+    use std::io::Write;
+
+    inliner_impl!();
+
+    impl<'a> CSSInliner<'a> {
+        /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a
+        /// string.
+        ///
+        /// # Errors
+        ///
+        /// Inlining might fail for the following reasons:
+        ///   - Missing stylesheet file;
+        ///   - Remote stylesheet is not available;
+        ///   - IO errors;
+        ///   - Internal CSS selector parsing error;
+        #[inline]
+        pub fn inline(&self, html: &str) -> Result<String> {
+            let mut out = build_output_buffer(html.len());
+            self.inline_to(html, &mut out)?;
+            Ok(String::from_utf8_lossy(&out).to_string())
+        }
+        /// Inline CSS & write the result to a generic writer. Use it if you want to write
+        /// the inlined document to a file.
+        ///
+        /// # Errors
+        ///
+        /// Inlining might fail for the following reasons:
+        ///   - Missing stylesheet file;
+        ///   - Remote stylesheet is not available;
+        ///   - IO errors;
+        ///   - Internal CSS selector parsing error;
+        #[inline]
+        pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
+            inline_to_impl!(self, html, target, |location| { self.options.resolver.retrieve_blocking(location)}, ?)
+        }
+    }
+    /// Shortcut for inlining CSS with default parameters.
+    ///
+    /// # Errors
+    ///
+    /// Inlining might fail for the following reasons:
+    ///   - Missing stylesheet file;
+    ///   - Remote stylesheet is not available;
+    ///   - IO errors;
+    ///   - Internal CSS selector parsing error;
+    #[inline]
+    pub fn inline(html: &str) -> Result<String> {
+        CSSInliner::default().inline(html)
+    }
+
+    /// Shortcut for inlining CSS with default parameters and writing the output to a generic writer.
+    ///
+    /// # Errors
+    ///
+    /// Inlining might fail for the following reasons:
+    ///   - Missing stylesheet file;
+    ///   - Remote stylesheet is not available;
+    ///   - IO errors;
+    ///   - Internal CSS selector parsing error;
+    #[inline]
+    pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
+        CSSInliner::default().inline_to(html, target)
+    }
 }
diff --git a/css-inline/src/main.rs b/css-inline/src/main.rs
index 699f1a04..22789250 100644
--- a/css-inline/src/main.rs
+++ b/css-inline/src/main.rs
@@ -6,7 +6,10 @@ fn main() {
 
 #[cfg(feature = "cli")]
 fn main() -> Result<(), Box<dyn std::error::Error>> {
-    use css_inline::{CSSInliner, DefaultStylesheetResolver, InlineOptions};
+    use css_inline::{
+        blocking::{CSSInliner, InlineOptions},
+        DefaultStylesheetResolver,
+    };
     use rayon::prelude::*;
     use std::{
         borrow::Cow,
diff --git a/css-inline/src/resolver.rs b/css-inline/src/resolver.rs
index c40eecc0..4f26e12a 100644
--- a/css-inline/src/resolver.rs
+++ b/css-inline/src/resolver.rs
@@ -1,25 +1,27 @@
 use crate::{InlineError, Result};
+use futures_util::FutureExt;
 use std::io::ErrorKind;
 
-/// Blocking way of resolving stylesheets from various sources.
+type AsyncResult<'a, T> = std::pin::Pin<Box<dyn futures_util::Future<Output = Result<T>> + 'a>>;
+
+/// Resolving stylesheets from various sources.
 pub trait StylesheetResolver: Send + Sync {
-    /// Retrieve a stylesheet from a network or local filesystem location.
+    /// Retrieve a stylesheet from a network or local filesystem location in a blocking way.
     ///
     /// # Errors
     ///
     /// Any network or filesystem related error, or an error during response parsing.
-    fn retrieve(&self, location: &str) -> Result<String> {
+    fn retrieve_blocking(&self, location: &str) -> Result<String> {
         if location.starts_with("https") | location.starts_with("http") {
-            #[cfg(feature = "http")]
+            #[cfg(feature = "http-blocking")]
             {
-                self.retrieve_from_url(location)
+                self.retrieve_from_url_blocking(location)
             }
-
-            #[cfg(not(feature = "http"))]
+            #[cfg(not(feature = "http-blocking"))]
             {
                 Err(InlineError::IO(std::io::Error::new(
                     ErrorKind::Unsupported,
-                    "Loading external URLs requires the `http` feature",
+                    "Loading external URLs requires the `http-blocking` feature",
                 )))
             }
         } else {
@@ -36,14 +38,60 @@ pub trait StylesheetResolver: Send + Sync {
             }
         }
     }
-    /// Retrieve a stylesheet from a network location.
+    /// Retrieve a stylesheet from a network location in a blocking way.
     ///
     /// # Errors
     ///
     /// Any network-related error, or an error during response parsing.
-    fn retrieve_from_url(&self, url: &str) -> Result<String> {
+    fn retrieve_from_url_blocking(&self, url: &str) -> Result<String> {
         Err(self.unsupported(&format!("Loading external URLs is not supported: {url}")))
     }
+    /// Retrieve a stylesheet from a network or local filesystem location in a non-blocking way.
+    ///
+    /// # Errors
+    ///
+    /// Any network or filesystem related error, or an error during response parsing.
+    fn retrieve<'a>(&'a self, location: &'a str) -> AsyncResult<'_, String> {
+        if location.starts_with("https") | location.starts_with("http") {
+            #[cfg(feature = "http")]
+            {
+                self.retrieve_from_url(location)
+            }
+            #[cfg(not(feature = "http"))]
+            {
+                async move {
+                    Err(InlineError::IO(std::io::Error::new(
+                        ErrorKind::Unsupported,
+                        "Loading external URLs requires the `http` feature",
+                    )))
+                }
+                .boxed_local()
+            }
+        } else {
+            async move {
+                #[cfg(feature = "file")]
+                {
+                    self.retrieve_from_path(location)
+                }
+                #[cfg(not(feature = "file"))]
+                {
+                    Err(InlineError::IO(std::io::Error::new(
+                        ErrorKind::Unsupported,
+                        "Loading local files requires the `file` feature",
+                    )))
+                }
+            }
+            .boxed_local()
+        }
+    }
+    /// Retrieve a stylesheet from a network location in a non-blocking way.
+    ///
+    /// # Errors
+    ///
+    /// Any network-related error, or an error during response parsing.
+    fn retrieve_from_url<'a>(&'a self, url: &'a str) -> AsyncResult<'_, String> {
+        async move { Err(self.unsupported(&format!("Loading external URLs is not supported: {url}"))) }.boxed_local()
+    }
     /// Retrieve a stylesheet from the local filesystem.
     ///
     /// # Errors
@@ -74,7 +122,23 @@ pub struct DefaultStylesheetResolver;
 
 impl StylesheetResolver for DefaultStylesheetResolver {
     #[cfg(feature = "http")]
-    fn retrieve_from_url(&self, url: &str) -> Result<String> {
+    fn retrieve_from_url<'a>(&'a self, url: &'a str) -> AsyncResult<'_, String> {
+        let into_error = |error| InlineError::Network {
+            error,
+            location: url.to_string(),
+        };
+        async move {
+            reqwest::get(url)
+                .await
+                .map_err(into_error)?
+                .text()
+                .await
+                .map_err(into_error)
+        }
+        .boxed_local()
+    }
+    #[cfg(feature = "http-blocking")]
+    fn retrieve_from_url_blocking(&self, url: &str) -> Result<String> {
         let into_error = |error| InlineError::Network {
             error,
             location: url.to_string(),
diff --git a/css-inline/tests/test_inlining.rs b/css-inline/tests/test_inlining.rs
index 7e5a59a8..09c97bd8 100644
--- a/css-inline/tests/test_inlining.rs
+++ b/css-inline/tests/test_inlining.rs
@@ -38,8 +38,8 @@ fn assert_http(inlined: Result<String, css_inline::InlineError>, expected: &str)
     }
 }
 
-#[test]
-fn no_existing_style() {
+#[tokio::test]
+async fn no_existing_style() {
     // When no "style" attributes exist
     assert_inlined!(
         style = r#"h1, h2 { color:red; }
@@ -56,8 +56,8 @@ p.footer { font-size: 1px}"#,
     )
 }
 
-#[test]
-fn ignore_inlining_attribute_tag() {
+#[tokio::test]
+async fn ignore_inlining_attribute_tag() {
     // When an HTML tag contains `data-css-inline="ignore"`
     assert_inlined!(
         style = "h1 { color:blue; }",
@@ -67,8 +67,8 @@ fn ignore_inlining_attribute_tag() {
     )
 }
 
-#[test]
-fn ignore_inlining_attribute_style() {
+#[tokio::test]
+async fn ignore_inlining_attribute_style() {
     // When a `style` tag contains `data-css-inline="ignore"`
     let html = r#"
 <html>
@@ -81,7 +81,7 @@ h1 { color: blue; }
 <h1>Big Text</h1>
 </body>
 </html>"#;
-    let result = inline(html).unwrap();
+    let result = inline(html).await.unwrap();
     // Then it should be skipped
     assert!(result.ends_with(
         r#"<body>
@@ -91,8 +91,8 @@ h1 { color: blue; }
     ))
 }
 
-#[test]
-fn ignore_inlining_attribute_link() {
+#[tokio::test]
+async fn ignore_inlining_attribute_link() {
     // When a `link` tag contains `data-css-inline="ignore"`
     let html = r#"
 <html>
@@ -103,7 +103,7 @@ fn ignore_inlining_attribute_link() {
 <h1>Big Text</h1>
 </body>
 </html>"#;
-    let result = inline(html).unwrap();
+    let result = inline(html).await.unwrap();
     // Then it should be skipped
     assert!(result.ends_with(
         r#"<body>
@@ -113,8 +113,8 @@ fn ignore_inlining_attribute_link() {
     ))
 }
 
-#[test]
-fn specificity_same_selector() {
+#[tokio::test]
+async fn specificity_same_selector() {
     assert_inlined!(
         style = r#"
 .test-class {
@@ -128,8 +128,8 @@ fn specificity_same_selector() {
     )
 }
 
-#[test]
-fn specificity_different_selectors() {
+#[tokio::test]
+async fn specificity_different_selectors() {
     assert_inlined!(
         style = r#"
 .test { padding-left: 16px; }
@@ -139,8 +139,8 @@ h1 { padding: 0; }"#,
     )
 }
 
-#[test]
-fn specificity_different_selectors_existing_style() {
+#[tokio::test]
+async fn specificity_different_selectors_existing_style() {
     assert_inlined!(
         style = r#"
 .test { padding-left: 16px; }
@@ -150,8 +150,8 @@ h1 { padding: 0; }"#,
     )
 }
 
-#[test]
-fn overlap_styles() {
+#[tokio::test]
+async fn overlap_styles() {
     // When two selectors match the same element
     assert_inlined!(
         style = r#"
@@ -168,12 +168,12 @@ a {
     )
 }
 
-#[test]
-fn simple_merge() {
+#[tokio::test]
+async fn simple_merge() {
     // When "style" attributes exist and collides with values defined in "style" tag
     let style = "h1 { color:red; }";
     let html = html!(style, r#"<h1 style="font-size: 1px">Big Text</h1>"#);
-    let inlined = inline(&html).unwrap();
+    let inlined = inline(&html).await.unwrap();
     // Then new styles should be merged with the existing ones
     let option_1 = html!(r#"<h1 style="font-size: 1px;color: red">Big Text</h1>"#);
     let option_2 = html!(r#"<h1 style="color: red;font-size: 1px">Big Text</h1>"#);
@@ -181,8 +181,8 @@ fn simple_merge() {
     assert!(valid, "{}", inlined);
 }
 
-#[test]
-fn overloaded_styles() {
+#[tokio::test]
+async fn overloaded_styles() {
     // When there is a style, applied to an ID
     assert_inlined!(
         style = "h1 { color: red; } #test { color: blue; }",
@@ -192,8 +192,8 @@ fn overloaded_styles() {
     )
 }
 
-#[test]
-fn important() {
+#[tokio::test]
+async fn important() {
     // `!important` rules should override existing inline styles
     assert_inlined!(
         style = "h1 { color: blue !important; }",
@@ -202,8 +202,8 @@ fn important() {
     )
 }
 
-#[test]
-fn important_no_rule_exists() {
+#[tokio::test]
+async fn important_no_rule_exists() {
     // `!important` rules should override existing inline styles
     assert_inlined!(
         style = "h1 { color: blue !important; }",
@@ -212,8 +212,8 @@ fn important_no_rule_exists() {
     )
 }
 
-#[test]
-fn font_family_quoted() {
+#[tokio::test]
+async fn font_family_quoted() {
     // When property value contains double quotes
     assert_inlined!(
         style = r#"h1 { font-family: "Open Sans", sans-serif; }"#,
@@ -223,8 +223,8 @@ fn font_family_quoted() {
     )
 }
 
-#[test]
-fn other_property_quoted() {
+#[tokio::test]
+async fn other_property_quoted() {
     // When property value contains double quotes
     assert_inlined!(
         style = r#"h1 { --bs-font-sant-serif: system-ui,-applie-system,"helvetica neue"; }"#,
@@ -234,8 +234,8 @@ fn other_property_quoted() {
     )
 }
 
-#[test]
-fn href_attribute_unchanged() {
+#[tokio::test]
+async fn href_attribute_unchanged() {
     // All HTML attributes should be serialized as is
     let html = r#"<html>
 <head>
@@ -246,7 +246,7 @@ fn href_attribute_unchanged() {
     <a href="https://example.org/test?a=b&c=d">Link</a>
 </body>
 </html>"#;
-    let inlined = inline(html).unwrap();
+    let inlined = inline(html).await.unwrap();
     assert_eq!(
         inlined,
         r#"<html><head>
@@ -260,8 +260,8 @@ fn href_attribute_unchanged() {
     );
 }
 
-#[test]
-fn complex_child_selector() {
+#[tokio::test]
+async fn complex_child_selector() {
     let html = r#"<html>
    <head>
       <style>.parent {
@@ -288,7 +288,7 @@ fn complex_child_selector() {
             </tbody>
          </table>
       </div></body></html>"#;
-    let inlined = inline(html).unwrap();
+    let inlined = inline(html).await.unwrap();
     assert_eq!(
         inlined,
         r#"<html><head>
@@ -311,8 +311,8 @@ fn complex_child_selector() {
     );
 }
 
-#[test]
-fn existing_styles() {
+#[tokio::test]
+async fn existing_styles() {
     // When there is a `style` attribute on a tag that contains a rule
     // And the `style` tag contains the same rule applicable to that tag
     assert_inlined!(
@@ -323,8 +323,8 @@ fn existing_styles() {
     )
 }
 
-#[test]
-fn existing_styles_multiple_tags() {
+#[tokio::test]
+async fn existing_styles_multiple_tags() {
     // When there are `style` attribute on tags that contains rules
     // And the `style` tag contains the same rule applicable to those tags
     assert_inlined!(
@@ -337,8 +337,8 @@ fn existing_styles_multiple_tags() {
     )
 }
 
-#[test]
-fn existing_styles_with_merge() {
+#[tokio::test]
+async fn existing_styles_with_merge() {
     // When there is a `style` attribute on a tag that contains a rule
     // And the `style` tag contains the same rule applicable to that tag
     // And there is a new rule in the `style` tag
@@ -351,8 +351,8 @@ fn existing_styles_with_merge() {
     )
 }
 
-#[test]
-fn existing_styles_with_merge_multiple_tags() {
+#[tokio::test]
+async fn existing_styles_with_merge_multiple_tags() {
     // When there are non-empty `style` attributes on tags
     // And the `style` tag contains the same rule applicable to those tags
     // And there is a new rule in the `style` tag
@@ -366,8 +366,8 @@ fn existing_styles_with_merge_multiple_tags() {
     )
 }
 
-#[test]
-fn remove_multiple_style_tags_without_inlining() {
+#[tokio::test]
+async fn remove_multiple_style_tags_without_inlining() {
     let html = r#"
 <html>
 <head>
@@ -395,7 +395,7 @@ a {
         .keep_style_tags(false)
         .inline_style_tags(false)
         .build();
-    let result = inliner.inline(html).unwrap();
+    let result = inliner.inline(html).await.unwrap();
     assert_eq!(
         result,
         r#"<html><head>
@@ -411,8 +411,8 @@ a {
     )
 }
 
-#[test]
-fn do_not_process_style_tag() {
+#[tokio::test]
+async fn do_not_process_style_tag() {
     let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>");
     let options = InlineOptions {
         inline_style_tags: false,
@@ -420,15 +420,15 @@ fn do_not_process_style_tag() {
         ..Default::default()
     };
     let inliner = CSSInliner::new(options);
-    let result = inliner.inline(&html).unwrap();
+    let result = inliner.inline(&html).await.unwrap();
     assert_eq!(
         result,
         "<html><head><style>h1 {background-color: blue;}</style></head><body><h1>Hello world!</h1></body></html>"
     )
 }
 
-#[test]
-fn do_not_process_style_tag_and_remove() {
+#[tokio::test]
+async fn do_not_process_style_tag_and_remove() {
     let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>");
     let options = InlineOptions {
         keep_style_tags: false,
@@ -436,15 +436,15 @@ fn do_not_process_style_tag_and_remove() {
         ..Default::default()
     };
     let inliner = CSSInliner::new(options);
-    let result = inliner.inline(&html).unwrap();
+    let result = inliner.inline(&html).await.unwrap();
     assert_eq!(
         result,
         "<html><head></head><body><h1>Hello world!</h1></body></html>"
     )
 }
 
-#[test]
-fn empty_style() {
+#[tokio::test]
+async fn empty_style() {
     // When the style tag is empty
     assert_inlined!(
         style = "",
@@ -454,8 +454,8 @@ fn empty_style() {
     )
 }
 
-#[test]
-fn media_query_ignore() {
+#[tokio::test]
+async fn media_query_ignore() {
     // When the style value includes @media query
     assert_inlined!(
         style = r#"@media screen and (max-width: 992px) {
@@ -471,25 +471,26 @@ fn media_query_ignore() {
 #[test_case("@wrong { color: --- }", "Invalid @ rule: wrong")]
 #[test_case("ttt { 123 }", "Unexpected token: CurlyBracketBlock")]
 #[test_case("----", "End of input")]
-fn invalid_rule(style: &str, expected: &str) {
+#[tokio::test]
+async fn invalid_rule(style: &str, expected: &str) {
     let html = html!(
         "h1 {background-color: blue;}",
         format!(r#"<h1 style="{}">Hello world!</h1>"#, style)
     );
-    let result = inline(&html);
+    let result = inline(&html).await;
     assert!(result.is_err());
     assert_eq!(result.unwrap_err().to_string(), expected);
 }
 
-#[test]
-fn remove_style_tag() {
+#[tokio::test]
+async fn remove_style_tag() {
     let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>");
-    let result = inline(&html).unwrap();
+    let result = inline(&html).await.unwrap();
     assert_eq!(result, "<html><head></head><body><h1 style=\"background-color: blue;\">Hello world!</h1></body></html>")
 }
 
-#[test]
-fn remove_multiple_style_tags() {
+#[tokio::test]
+async fn remove_multiple_style_tags() {
     let html = r#"
 <html>
 <head>
@@ -513,7 +514,7 @@ a {
 </body>
 </html>
     "#;
-    let result = inline(html).unwrap();
+    let result = inline(html).await.unwrap();
     assert_eq!(
         result,
         r#"<html><head>
@@ -529,8 +530,8 @@ a {
     )
 }
 
-#[test]
-fn extra_css() {
+#[tokio::test]
+async fn extra_css() {
     let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>");
     let options = InlineOptions {
         inline_style_tags: false,
@@ -538,15 +539,15 @@ fn extra_css() {
         ..Default::default()
     };
     let inliner = CSSInliner::new(options);
-    let result = inliner.inline(&html).unwrap();
+    let result = inliner.inline(&html).await.unwrap();
     assert_eq!(
         result,
         "<html><head></head><body><h1 style=\"background-color: green;\">Hello world!</h1></body></html>"
     )
 }
 
-#[test]
-fn remote_file_stylesheet() {
+#[tokio::test]
+async fn remote_file_stylesheet() {
     let html = r#"
 <html>
 <head>
@@ -561,7 +562,7 @@ h2 { color: red; }
 <h2>Smaller Text</h2>
 </body>
 </html>"#;
-    let inlined = inline(html);
+    let inlined = inline(html).await;
     assert_file(
         inlined,
         r#"<body>
@@ -572,8 +573,8 @@ h2 { color: red; }
     );
 }
 
-#[test]
-fn missing_stylesheet() {
+#[tokio::test]
+async fn missing_stylesheet() {
     let html = r#"
 <html>
 <head>
@@ -583,7 +584,7 @@ fn missing_stylesheet() {
 <h1>Big Text</h1>
 </body>
 </html>"#;
-    let inlined = inline(html);
+    let inlined = inline(html).await;
     #[cfg(feature = "file")]
     {
         assert_eq!(
@@ -597,8 +598,8 @@ fn missing_stylesheet() {
     }
 }
 
-#[test]
-fn remote_file_stylesheet_disable() {
+#[tokio::test]
+async fn remote_file_stylesheet_disable() {
     let html = r#"
 <html>
 <head>
@@ -613,7 +614,7 @@ h2 { color: red; }
 <h2>Smaller Text</h2>
 </body>
 </html>"#;
-    let inlined = inline(html);
+    let inlined = inline(html).await;
     assert_file(
         inlined,
         r#"<body>
@@ -624,8 +625,8 @@ h2 { color: red; }
     );
 }
 
-#[test]
-fn remote_network_stylesheet() {
+#[tokio::test]
+async fn remote_network_stylesheet() {
     let html = r#"
 <html>
 <head>
@@ -640,7 +641,7 @@ h2 { color: red; }
 <h2>Smaller Text</h2>
 </body>
 </html>"#;
-    let inlined = inline(html);
+    let inlined = inline(html).await;
     assert_http(
         inlined,
         r#"<body>
@@ -651,8 +652,8 @@ h2 { color: red; }
     );
 }
 
-#[test]
-fn remote_network_stylesheet_invalid_url() {
+#[tokio::test]
+async fn remote_network_stylesheet_invalid_url() {
     let html = r#"
 <html>
 <head>
@@ -661,7 +662,7 @@ fn remote_network_stylesheet_invalid_url() {
 <body>
 </body>
 </html>"#;
-    let error = inline(html).expect_err("Should fail");
+    let error = inline(html).await.expect_err("Should fail");
     #[cfg(feature = "http")]
     let expected = "builder error: empty host: http:";
     #[cfg(not(feature = "http"))]
@@ -669,8 +670,8 @@ fn remote_network_stylesheet_invalid_url() {
     assert_eq!(error.to_string(), expected);
 }
 
-#[test]
-fn remote_network_stylesheet_same_scheme() {
+#[tokio::test]
+async fn remote_network_stylesheet_same_scheme() {
     let html = r#"
 <html>
 <head>
@@ -688,7 +689,7 @@ h2 { color: red; }
     let inliner = CSSInliner::options()
         .base_url(Some(Url::parse("http://127.0.0.1:1234").unwrap()))
         .build();
-    let inlined = inliner.inline(html);
+    let inlined = inliner.inline(html).await;
     assert_http(
         inlined,
         r#"<body>
@@ -699,8 +700,8 @@ h2 { color: red; }
     );
 }
 
-#[test]
-fn remote_network_relative_stylesheet() {
+#[tokio::test]
+async fn remote_network_relative_stylesheet() {
     let html = r#"
 <html>
 <head>
@@ -718,7 +719,7 @@ h2 { color: red; }
     let inliner = CSSInliner::options()
         .base_url(Some(Url::parse("http://127.0.0.1:1234").unwrap()))
         .build();
-    let inlined = inliner.inline(html);
+    let inlined = inliner.inline(html).await;
     assert_http(
         inlined,
         r#"<body>
@@ -729,8 +730,8 @@ h2 { color: red; }
     );
 }
 
-#[test]
-fn file_scheme() {
+#[tokio::test]
+async fn file_scheme() {
     let html = r#"
 <html>
 <head>
@@ -750,7 +751,7 @@ h2 { color: red; }
         ..Default::default()
     };
     let inliner = CSSInliner::new(options);
-    let inlined = inliner.inline(html);
+    let inlined = inliner.inline(html).await;
     assert_file(
         inlined,
         r#"<body>
@@ -761,8 +762,8 @@ h2 { color: red; }
     );
 }
 
-#[test]
-fn customize_inliner() {
+#[tokio::test]
+async fn customize_inliner() {
     let options = InlineOptions {
         load_remote_stylesheets: false,
         ..Default::default()
@@ -774,8 +775,8 @@ fn customize_inliner() {
     assert_eq!(options.preallocate_node_capacity, 25);
 }
 
-#[test]
-fn use_builder() {
+#[tokio::test]
+async fn use_builder() {
     let url = Url::parse("https://api.example.com").unwrap();
     let _ = CSSInliner::options()
         .keep_style_tags(true)
@@ -785,19 +786,19 @@ fn use_builder() {
         .build();
 }
 
-#[test]
-fn inline_to() {
+#[tokio::test]
+async fn inline_to() {
     let html = html!("h1 { color: blue }", r#"<h1>Big Text</h1>"#);
     let mut out = Vec::new();
-    css_inline::inline_to(&html, &mut out).unwrap();
+    css_inline::inline_to(&html, &mut out).await.unwrap();
     assert_eq!(
         String::from_utf8_lossy(&out),
         "<html><head></head><body><h1 style=\"color: blue;\">Big Text</h1></body></html>"
     )
 }
 
-#[test]
-fn keep_style_tags() {
+#[tokio::test]
+async fn keep_style_tags() {
     let inliner = CSSInliner::options().keep_style_tags(true).build();
     let html = r#"
 <html>
@@ -810,12 +811,12 @@ h2 { color: red; }
 <h2></h2>
 </body>
 </html>"#;
-    let inlined = inliner.inline(html).unwrap();
+    let inlined = inliner.inline(html).await.unwrap();
     assert_eq!(inlined, "<html><head>\n<style>\nh2 { color: red; }\n</style>\n</head>\n<body>\n<h2 style=\"color: red;\"></h2>\n\n</body></html>");
 }
 
-#[test]
-fn keep_link_tags() {
+#[tokio::test]
+async fn keep_link_tags() {
     let inliner = CSSInliner::options()
         .base_url(Some(Url::parse("http://127.0.0.1:1234").unwrap()))
         .keep_link_tags(true)
@@ -829,7 +830,7 @@ fn keep_link_tags() {
 <h1></h1>
 </body>
 </html>"#;
-    let inlined = inliner.inline(html);
+    let inlined = inliner.inline(html).await;
     assert_http(
         inlined,
         "<html><head>\n<link href=\"external.css\" rel=\"stylesheet\">\n</head>\n<body>\n<h1 style=\"color: blue;\"></h1>\n\n</body></html>",
diff --git a/css-inline/tests/test_selectors.rs b/css-inline/tests/test_selectors.rs
index 75cd2f2d..f3e144ca 100644
--- a/css-inline/tests/test_selectors.rs
+++ b/css-inline/tests/test_selectors.rs
@@ -2,8 +2,8 @@
 mod utils;
 
 // Most of the following tests are ported to Rust from https://github.com/rennat/pynliner
-#[test]
-fn identical_element() {
+#[tokio::test]
+async fn identical_element() {
     assert_inlined!(
         style = r#"
         .text-right {
@@ -18,8 +18,8 @@ fn identical_element() {
     )
 }
 
-#[test]
-fn is_or_prefixed_by() {
+#[tokio::test]
+async fn is_or_prefixed_by() {
     assert_inlined!(
         style = r#"[data-type|="thing"] {color: red;}"#,
         body = r#"<span data-type="thing">1</span>"#,
@@ -32,8 +32,8 @@ fn is_or_prefixed_by() {
     )
 }
 
-#[test]
-fn contains() {
+#[tokio::test]
+async fn contains() {
     assert_inlined!(
         style = r#"[data-type*="i"] {color: red;}"#,
         body = r#"<span data-type="thing">1</span>"#,
@@ -41,8 +41,8 @@ fn contains() {
     )
 }
 
-#[test]
-fn has_class_unicode() {
+#[tokio::test]
+async fn has_class_unicode() {
     assert_inlined!(
         style = r#".тест {color: red}"#,
         body = r#"<span class="тест">1</span>"#,
@@ -50,8 +50,8 @@ fn has_class_unicode() {
     )
 }
 
-#[test]
-fn has_class_short() {
+#[tokio::test]
+async fn has_class_short() {
     assert_inlined!(
         style = r#".t {color: red}"#,
         body = r#"<span class="test">1</span>"#,
@@ -59,8 +59,8 @@ fn has_class_short() {
     )
 }
 
-#[test]
-fn has_class_multiple() {
+#[tokio::test]
+async fn has_class_multiple() {
     assert_inlined!(
         style = r#".t {color: red}"#,
         body = r#"<span class="t e s t">1</span>"#,
@@ -68,24 +68,24 @@ fn has_class_multiple() {
     )
 }
 
-#[test]
-fn ends_with() {
+#[tokio::test]
+async fn ends_with() {
     assert_inlined!(
         style = r#"[data-type$="ng"] {color: red;}"#,
         body = r#"<span data-type="thing">1</span>"#,
         expected = r#"<span data-type="thing" style="color: red;">1</span>"#
     )
 }
-#[test]
-fn starts_with() {
+#[tokio::test]
+async fn starts_with() {
     assert_inlined!(
         style = r#"[data-type^="th"] {color: red;}"#,
         body = r#"<span data-type="thing">1</span>"#,
         expected = r#"<span data-type="thing" style="color: red;">1</span>"#
     )
 }
-#[test]
-fn one_of() {
+#[tokio::test]
+async fn one_of() {
     assert_inlined!(
         style = r#"[data-type~="thing1"] {color: red;}"#,
         body = r#"<span data-type="thing1 thing2">1</span>"#,
@@ -97,8 +97,8 @@ fn one_of() {
         expected = r#"<span data-type="thing1 thing2" style="color: red;">1</span>"#
     )
 }
-#[test]
-fn equals() {
+#[tokio::test]
+async fn equals() {
     assert_inlined!(
         style = r#"[data-type="thing"] {color: red;}"#,
         body = r#"<span data-type="thing">1</span>"#,
@@ -110,16 +110,16 @@ fn equals() {
         expected = r#"<span data-type="thing" style="color: red;">1</span>"#
     )
 }
-#[test]
-fn exists() {
+#[tokio::test]
+async fn exists() {
     assert_inlined!(
         style = r#"[data-type] {color: red;}"#,
         body = r#"<span data-type="thing">1</span>"#,
         expected = r#"<span data-type="thing" style="color: red;">1</span>"#
     )
 }
-#[test]
-fn specificity() {
+#[tokio::test]
+async fn specificity() {
     assert_inlined!(
         style = r#"div,a,b,c,d,e,f,g,h,i,j { color: red; } .foo { color: blue; }"#,
         body = r#"<div class="foo"></div>"#,
@@ -127,88 +127,88 @@ fn specificity() {
     )
 }
 
-#[test]
-fn first_child_descendant_selector_complex_dom() {
+#[tokio::test]
+async fn first_child_descendant_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 :first-child { color: red; }"#,
         body = r#"<h1><div><span>Hello World!</span></div><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><div style="color: red;"><span style="color: red;">Hello World!</span></div><p>foo</p><div class="barclass"><span style="color: red;">baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn last_child_descendant_selector() {
+#[tokio::test]
+async fn last_child_descendant_selector() {
     assert_inlined!(
         style = r#"h1 :last-child { color: red; }"#,
         body = r#"<h1><div><span>Hello World!</span></div></h1>"#,
         expected = r#"<h1><div style="color: red;"><span style="color: red;">Hello World!</span></div></h1>"#
     )
 }
-#[test]
-fn first_child_descendant_selector() {
+#[tokio::test]
+async fn first_child_descendant_selector() {
     assert_inlined!(
         style = r#"h1 :first-child { color: red; }"#,
         body = r#"<h1><div><span>Hello World!</span></div></h1>"#,
         expected = r#"<h1><div style="color: red;"><span style="color: red;">Hello World!</span></div></h1>"#
     )
 }
-#[test]
-fn child_with_first_child_and_unmatched_class_selector_complex_dom() {
+#[tokio::test]
+async fn child_with_first_child_and_unmatched_class_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > .hello:first-child { color: green; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn child_with_first_child_and_class_selector_complex_dom() {
+#[tokio::test]
+async fn child_with_first_child_and_class_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > .hello:first-child { color: green; }"#,
         body = r#"<h1><span class="hello">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span class="hello" style="color: green;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn nested_child_with_first_child_override_selector_complex_dom() {
+#[tokio::test]
+async fn nested_child_with_first_child_override_selector_complex_dom() {
     assert_inlined!(
         style = r#"div > div > * { color: green; } div > div > :first-child { color: red; }"#,
         body = r#"<div><div><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></div></div>"#,
         expected = r#"<div><div><span style="color: red;">Hello World!</span><p style="color: green;">foo</p><div class="barclass" style="color: green;"><span style="color: red;">baz</span>bar</div></div></div>"#
     )
 }
-#[test]
-fn child_with_first_and_last_child_override_selector() {
+#[tokio::test]
+async fn child_with_first_and_last_child_override_selector() {
     assert_inlined!(
         style = r#"p > * { color: green; } p > :first-child:last-child { color: red; }"#,
         body = r#"<p><span>Hello World!</span></p>"#,
         expected = r#"<p><span style="color: red;">Hello World!</span></p>"#
     )
 }
-#[test]
-fn id_el_child_with_first_child_override_selector_complex_dom() {
+#[tokio::test]
+async fn id_el_child_with_first_child_override_selector_complex_dom() {
     assert_inlined!(
         style = r#"#abc > * { color: green; } #abc > :first-child { color: red; }"#,
         body = r#"<div id="abc"><span class="cde">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></div>"#,
         expected = r#"<div id="abc"><span class="cde" style="color: red;">Hello World!</span><p style="color: green;">foo</p><div class="barclass" style="color: green;"><span>baz</span>bar</div></div>"#
     )
 }
-#[test]
-fn child_with_first_child_override_selector_complex_dom() {
+#[tokio::test]
+async fn child_with_first_child_override_selector_complex_dom() {
     assert_inlined!(
         style = r#"div > * { color: green; } div > :first-child { color: red; }"#,
         body = r#"<div><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></div>"#,
         expected = r#"<div><span style="color: red;">Hello World!</span><p style="color: green;">foo</p><div class="barclass" style="color: green;"><span style="color: red;">baz</span>bar</div></div>"#
     )
 }
-#[test]
-fn child_follow_by_last_child_selector_complex_dom() {
+#[tokio::test]
+async fn child_follow_by_last_child_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > :last-child { color: red; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass" style="color: red;"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn parent_pseudo_selector() {
+#[tokio::test]
+async fn parent_pseudo_selector() {
     assert_inlined!(
         style = r#"span:last-child span { color: red; }"#,
         body = r#"<h1><span><span>Hello World!</span></span></h1>"#,
@@ -225,8 +225,8 @@ fn parent_pseudo_selector() {
         expected = r#"<h1><span><span>Hello World!</span></span><span>nope</span></h1>"#
     )
 }
-#[test]
-fn multiple_pseudo_selectors() {
+#[tokio::test]
+async fn multiple_pseudo_selectors() {
     assert_inlined!(
         style = r#"span:first-child:last-child { color: red; }"#,
         body = r#"<h1><span>Hello World!</span></h1>"#,
@@ -238,32 +238,32 @@ fn multiple_pseudo_selectors() {
         expected = r#"<h1><span>Hello World!</span><span>again!</span></h1>"#
     )
 }
-#[test]
-fn last_child_selector() {
+#[tokio::test]
+async fn last_child_selector() {
     assert_inlined!(
         style = r#"h1 > :last-child { color: red; }"#,
         body = r#"<h1><span>Hello World!</span></h1>"#,
         expected = r#"<h1><span style="color: red;">Hello World!</span></h1>"#
     )
 }
-#[test]
-fn child_follow_by_first_child_selector_complex_dom() {
+#[tokio::test]
+async fn child_follow_by_first_child_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > :first-child { color: red; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span style="color: red;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn child_follow_by_first_child_selector_with_comments() {
+#[tokio::test]
+async fn child_follow_by_first_child_selector_with_comments() {
     assert_inlined!(
         style = r#"h1 > :first-child { color: red; }"#,
         body = r#"<h1> <!-- enough said --><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1> <!-- enough said --><span style="color: red;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn child_follow_by_first_child_selector_with_white_spaces() {
+#[tokio::test]
+async fn child_follow_by_first_child_selector_with_white_spaces() {
     assert_inlined!(
         style = r#"h1 > :first-child { color: red; }"#,
         body = r#"<h1> <span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
@@ -271,8 +271,8 @@ fn child_follow_by_first_child_selector_with_white_spaces() {
     )
 }
 
-#[test]
-fn child_follow_by_adjacent_selector_complex_dom() {
+#[tokio::test]
+async fn child_follow_by_adjacent_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > span + p { color: red; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
@@ -280,88 +280,88 @@ fn child_follow_by_adjacent_selector_complex_dom() {
     )
 }
 
-#[test]
-fn unknown_pseudo_selector() {
+#[tokio::test]
+async fn unknown_pseudo_selector() {
     assert_inlined!(
         style = r#"h1 > span:css4-selector { color: red; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn adjacent_selector() {
+#[tokio::test]
+async fn adjacent_selector() {
     assert_inlined!(
         style = r#"h1 + h2 { color: red; }"#,
         body = r#"<h1>Hello World!</h1><h2>How are you?</h2>"#,
         expected = r#"<h1>Hello World!</h1><h2 style="color: red;">How are you?</h2>"#
     )
 }
-#[test]
-fn child_all_selector_complex_dom() {
+#[tokio::test]
+async fn child_all_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > * { color: red; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span style="color: red;">Hello World!</span><p style="color: red;">foo</p><div class="barclass" style="color: red;"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn child_selector_complex_dom() {
+#[tokio::test]
+async fn child_selector_complex_dom() {
     assert_inlined!(
         style = r#"h1 > span { color: red; }"#,
         body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#,
         expected = r#"<h1><span style="color: red;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#
     )
 }
-#[test]
-fn nested_child_selector() {
+#[tokio::test]
+async fn nested_child_selector() {
     assert_inlined!(
         style = r#"div > h1 > span { color: red; }""#,
         body = r#"<div><h1><span>Hello World!</span></h1></div>"#,
         expected = r#"<div><h1><span style="color: red;">Hello World!</span></h1></div>"#
     )
 }
-#[test]
-fn child_selector() {
+#[tokio::test]
+async fn child_selector() {
     assert_inlined!(
         style = r#"h1 > span { color: red; }"#,
         body = r#"<h1><span>Hello World!</span></h1>"#,
         expected = r#"<h1><span style="color: red;">Hello World!</span></h1>"#
     )
 }
-#[test]
-fn descendant_selector() {
+#[tokio::test]
+async fn descendant_selector() {
     assert_inlined!(
         style = r#"h1 span { color: red; }"#,
         body = r#"<h1><span>Hello World!</span></h1>"#,
         expected = r#"<h1><span style="color: red;">Hello World!</span></h1>"#
     )
 }
-#[test]
-fn combination_selector() {
+#[tokio::test]
+async fn combination_selector() {
     assert_inlined!(
         style = r#"h1#a.b { color: red; }"#,
         body = r#"<h1 id="a" class="b">Hello World!</h1>"#,
         expected = r#"<h1 class="b" id="a" style="color: red;">Hello World!</h1>"#
     )
 }
-#[test]
-fn conflicting_multiple_class_selector() {
+#[tokio::test]
+async fn conflicting_multiple_class_selector() {
     assert_inlined!(
         style = r#"h1.a.b { color: red; }"#,
         body = r#"<h1 class="a b">Hello World!</h1><h1 class="a">I should not be changed</h1>"#,
         expected = r#"<h1 class="a b" style="color: red;">Hello World!</h1><h1 class="a">I should not be changed</h1>"#
     )
 }
-#[test]
-fn multiple_class_selector() {
+#[tokio::test]
+async fn multiple_class_selector() {
     assert_inlined!(
         style = r#"h1.a.b { color: red; }"#,
         body = r#"<h1 class="a b">Hello World!</h1>"#,
         expected = r#"<h1 class="a b" style="color: red;">Hello World!</h1>"#
     )
 }
-#[test]
-fn missing_link_descendant_selector() {
+#[tokio::test]
+async fn missing_link_descendant_selector() {
     assert_inlined!(
         style = r#"#a b i { color: red }"#,
         body = r#"<div id="a"><i>x</i></div>"#,
@@ -369,8 +369,8 @@ fn missing_link_descendant_selector() {
     )
 }
 
-#[test]
-fn comma_specificity() {
+#[tokio::test]
+async fn comma_specificity() {
     assert_inlined!(
         style = r#"i, i { color: red; } i { color: blue; }"#,
         body = r#"<i>howdy</i>"#,
@@ -378,8 +378,8 @@ fn comma_specificity() {
     )
 }
 
-#[test]
-fn overwrite_comma() {
+#[tokio::test]
+async fn overwrite_comma() {
     assert_inlined!(
         style = r#"h1,h2,h3 {color: #000;}"#,
         body = r#"<h1 style="color: #fff">Foo</h1><h3 style="color: #fff">Foo</h3>"#,
diff --git a/css-inline/tests/utils.rs b/css-inline/tests/utils.rs
index bebc768b..4fa4358e 100644
--- a/css-inline/tests/utils.rs
+++ b/css-inline/tests/utils.rs
@@ -15,7 +15,7 @@ macro_rules! html {
 macro_rules! assert_inlined {
     (style = $style: expr, body = $body: expr, expected = $expected: expr) => {{
         let html = html!($style, $body);
-        let inlined = css_inline::inline(&html).unwrap();
+        let inlined = css_inline::inline(&html).await.unwrap();
         assert_eq!(inlined, html!($expected))
     }};
 }
diff --git a/profiler/src/main.rs b/profiler/src/main.rs
index 79eac16b..c709dacc 100644
--- a/profiler/src/main.rs
+++ b/profiler/src/main.rs
@@ -35,7 +35,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         for _ in 0..args.iterations {
             #[cfg(feature = "dhat-heap")]
             let _profiler = dhat::Profiler::new_heap();
-            css_inline::inline_to(&benchmark.html, &mut output).expect("Inlining failed");
+            css_inline::blocking::inline_to(&benchmark.html, &mut output).expect("Inlining failed");
             output.clear();
         }
     } else {