diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs
index 7cd03520c8..b1ac7fa22f 100644
--- a/components/config/src/config/markup.rs
+++ b/components/config/src/config/markup.rs
@@ -51,6 +51,8 @@ pub struct Markdown {
     /// The compiled extra themes into a theme set
     #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
     pub extra_theme_set: Arc<Option<ThemeSet>>,
+    /// Add loading="lazy" decoding="async" to img tags. When turned on, the alt text must be plain text. Defaults to false
+    pub lazy_async_image: bool,
 }
 
 impl Markdown {
@@ -204,6 +206,7 @@ impl Default for Markdown {
             extra_syntaxes_and_themes: vec![],
             extra_syntax_set: None,
             extra_theme_set: Arc::new(None),
+            lazy_async_image: false,
         }
     }
 }
diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs
index d773a21f1f..a6cb3beca6 100644
--- a/components/markdown/src/markdown.rs
+++ b/components/markdown/src/markdown.rs
@@ -252,6 +252,8 @@ pub fn markdown_to_html(
 
     let mut stop_next_end_p = false;
 
+    let lazy_async_image = context.config.markdown.lazy_async_image;
+
     let mut opts = Options::empty();
     let mut has_summary = false;
     opts.insert(Options::ENABLE_TABLES);
@@ -387,13 +389,35 @@ pub fn markdown_to_html(
                     events.push(Event::Html("</code></pre>\n".into()));
                 }
                 Event::Start(Tag::Image(link_type, src, title)) => {
-                    if is_colocated_asset_link(&src) {
+                    let link = if is_colocated_asset_link(&src) {
                         let link = format!("{}{}", context.current_page_permalink, &*src);
-                        events.push(Event::Start(Tag::Image(link_type, link.into(), title)));
+                        link.into()
                     } else {
-                        events.push(Event::Start(Tag::Image(link_type, src, title)));
-                    }
+                        src
+                    };
+
+                    events.push(if lazy_async_image {
+                        let mut img_before_alt: String = "<img src=\"".to_string();
+                        cmark::escape::escape_href(&mut img_before_alt, &link)
+                            .expect("Could not write to buffer");
+                        if !title.is_empty() {
+                            img_before_alt
+                                .write_str("\" title=\"")
+                                .expect("Could not write to buffer");
+                            cmark::escape::escape_href(&mut img_before_alt, &title)
+                                .expect("Could not write to buffer");
+                        }
+                        img_before_alt.write_str("\" alt=\"").expect("Could not write to buffer");
+                        Event::Html(img_before_alt.into())
+                    } else {
+                        Event::Start(Tag::Image(link_type, link, title))
+                    });
                 }
+                Event::End(Tag::Image(..)) => events.push(if lazy_async_image {
+                    Event::Html("\" loading=\"lazy\" decoding=\"async\" />".into())
+                } else {
+                    event
+                }),
                 Event::Start(Tag::Link(link_type, link, title)) if link.is_empty() => {
                     error = Some(Error::msg("There is a link that is missing a URL"));
                     events.push(Event::Start(Tag::Link(link_type, "#".into(), title)));
diff --git a/components/markdown/tests/img.rs b/components/markdown/tests/img.rs
new file mode 100644
index 0000000000..c34713a9d7
--- /dev/null
+++ b/components/markdown/tests/img.rs
@@ -0,0 +1,33 @@
+mod common;
+use config::Config;
+
+#[test]
+fn can_transform_image() {
+    let cases = vec![
+        "![haha](https://example.com/abc.jpg)",
+        "![](https://example.com/abc.jpg)",
+        "![ha\"h>a](https://example.com/abc.jpg)",
+        "![__ha__*ha*](https://example.com/abc.jpg)",
+        "![ha[ha](https://example.com)](https://example.com/abc.jpg)",
+    ];
+
+    let body = common::render(&cases.join("\n")).unwrap().body;
+    insta::assert_snapshot!(body);
+}
+
+#[test]
+fn can_add_lazy_loading_and_async_decoding() {
+    let cases = vec![
+        "![haha](https://example.com/abc.jpg)",
+        "![](https://example.com/abc.jpg)",
+        "![ha\"h>a](https://example.com/abc.jpg)",
+        "![__ha__*ha*](https://example.com/abc.jpg)",
+        "![ha[ha](https://example.com)](https://example.com/abc.jpg)",
+    ];
+
+    let mut config = Config::default_for_test();
+    config.markdown.lazy_async_image = true;
+
+    let body = common::render_with_config(&cases.join("\n"), config).unwrap().body;
+    insta::assert_snapshot!(body);
+}
diff --git a/components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap b/components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap
new file mode 100644
index 0000000000..ed179b4043
--- /dev/null
+++ b/components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap
@@ -0,0 +1,10 @@
+---
+source: components/markdown/tests/img.rs
+expression: body
+---
+<p><img src="https://example.com/abc.jpg" alt="haha" loading="lazy" decoding="async" />
+<img src="https://example.com/abc.jpg" alt="" loading="lazy" decoding="async" />
+<img src="https://example.com/abc.jpg" alt="ha&quot;h&gt;a" loading="lazy" decoding="async" />
+<img src="https://example.com/abc.jpg" alt="<strong>ha</strong><em>ha</em>" loading="lazy" decoding="async" />
+<img src="https://example.com/abc.jpg" alt="ha<a href="https://example.com">ha</a>" loading="lazy" decoding="async" /></p>
+
diff --git a/components/markdown/tests/snapshots/img__can_transform_image.snap b/components/markdown/tests/snapshots/img__can_transform_image.snap
new file mode 100644
index 0000000000..5ad51f586a
--- /dev/null
+++ b/components/markdown/tests/snapshots/img__can_transform_image.snap
@@ -0,0 +1,10 @@
+---
+source: components/markdown/tests/img.rs
+expression: body
+---
+<p><img src="https://example.com/abc.jpg" alt="haha" />
+<img src="https://example.com/abc.jpg" alt="" />
+<img src="https://example.com/abc.jpg" alt="ha&quot;h&gt;a" />
+<img src="https://example.com/abc.jpg" alt="haha" />
+<img src="https://example.com/abc.jpg" alt="haha" /></p>
+
diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md
index b2ff781f6a..ac85b72426 100644
--- a/docs/content/documentation/getting-started/configuration.md
+++ b/docs/content/documentation/getting-started/configuration.md
@@ -122,6 +122,11 @@ external_links_no_referrer = false
 # For example, `...` into `…`, `"quote"` into `“curly”` etc
 smart_punctuation = false
 
+# Whether to set decoding="async" and loading="lazy" for all images
+# When turned on, the alt text must be plain text.
+# For example, `![xx](...)` is ok but `![*x*x](...)` isn’t ok
+lazy_async_image = false
+
 # Configuration of the link checker.
 [link_checker]
 # Skip link checking for external URLs that start with these prefixes