diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 064392c0ab2..8a666e79177 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -12,6 +12,7 @@
### Feats
+- 🌟 `n-image` adds `lazy` prop, closes [#3055](https://github.com/TuSimple/naive-ui/issues/3055).
- Exports `NTooltipInst` type.
- `n-data-table` adds `render-cell` prop, closes [#3095](https://github.com/TuSimple/naive-ui/issues/3095).
- `n-space` adds `wrap-item` prop.
diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md
index 09f4e7dc08b..b8c81c51eea 100644
--- a/CHANGELOG.zh-CN.md
+++ b/CHANGELOG.zh-CN.md
@@ -12,6 +12,7 @@
### Feats
+- 🌟 `n-image` 新增 `lazy` å±žæ€§ï¼Œå…³é— [#3055](https://github.com/TuSimple/naive-ui/issues/3055)
- 导出 `NTooltipInst` 类型
- `n-data-table` 新增 `render-cell` å±žæ€§ï¼Œå…³é— [#3095](https://github.com/TuSimple/naive-ui/issues/3095)
- `n-space` 新增 `wrap-item` 属性
diff --git a/src/image/demos/enUS/index.demo-entry.md b/src/image/demos/enUS/index.demo-entry.md
index 41695c8bfaf..ef5b969cfc7 100644
--- a/src/image/demos/enUS/index.demo-entry.md
+++ b/src/image/demos/enUS/index.demo-entry.md
@@ -11,6 +11,7 @@ error.vue
preview-disabled.vue
custom.vue
tooltip.vue
+lazy.vue
```
## API
diff --git a/src/image/demos/enUS/lazy.demo.vue b/src/image/demos/enUS/lazy.demo.vue
new file mode 100644
index 00000000000..09a19d24989
--- /dev/null
+++ b/src/image/demos/enUS/lazy.demo.vue
@@ -0,0 +1,16 @@
+
+# lazyLoad
+
+
+
+
+
diff --git a/src/image/demos/zhCN/index.demo-entry.md b/src/image/demos/zhCN/index.demo-entry.md
index 71595904037..ded951aa4c1 100644
--- a/src/image/demos/zhCN/index.demo-entry.md
+++ b/src/image/demos/zhCN/index.demo-entry.md
@@ -12,6 +12,7 @@ preview-disabled.vue
custom.vue
tooltip.vue
full-debug.vue
+lazy.vue
```
## API
diff --git a/src/image/demos/zhCN/lazy.demo.vue b/src/image/demos/zhCN/lazy.demo.vue
new file mode 100644
index 00000000000..58fda923f30
--- /dev/null
+++ b/src/image/demos/zhCN/lazy.demo.vue
@@ -0,0 +1,16 @@
+
+# æ‡’åŠ è½½
+
+
+
+
+
diff --git a/src/image/src/Image.tsx b/src/image/src/Image.tsx
index 8e41129b7cf..eb7927dbcbf 100644
--- a/src/image/src/Image.tsx
+++ b/src/image/src/Image.tsx
@@ -7,7 +7,9 @@ import {
toRef,
watchEffect,
ImgHTMLAttributes,
- onMounted
+ onMounted,
+ onBeforeUnmount,
+ watchPostEffect
} from 'vue'
import NImagePreview from './ImagePreview'
import type { ImagePreviewInst } from './ImagePreview'
@@ -15,6 +17,7 @@ import { imageGroupInjectionKey } from './ImageGroup'
import type { ExtractPublicPropTypes } from '../../_utils'
import { useConfig } from '../../_mixins'
import { imagePreviewSharedProps } from './interface'
+import { imgObserverHandler, imgUnobserverHandler } from './utils'
export interface ImageInst {
click: () => void
@@ -24,6 +27,10 @@ const imageProps = {
alt: String,
height: [String, Number] as PropType,
imgProps: Object as PropType,
+ lazy: Boolean,
+ lazyOptions: Object as PropType<{
+ root: string
+ }>,
objectFit: {
type: String as PropType<
'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
@@ -77,6 +84,21 @@ export default defineComponent({
imageGroupHandle?.groupId || ''
)
})
+
+ watchPostEffect(() => {
+ if (props.lazy) {
+ imgObserverHandler(imageRef.value, props.lazyOptions?.root)
+ } else {
+ imgUnobserverHandler(imageRef.value)
+ }
+ })
+
+ onBeforeUnmount(() => {
+ if (props.lazy) {
+ imgUnobserverHandler(imageRef.value)
+ }
+ })
+
watchEffect(() => {
void props.src
void props.imgProps?.src
@@ -112,7 +134,13 @@ export default defineComponent({
ref="imageRef"
width={this.width || imgProps.width}
height={this.height || imgProps.height}
- src={this.showError ? this.fallbackSrc : this.src || imgProps.src}
+ src={
+ this.showError
+ ? this.fallbackSrc
+ : this.lazy
+ ? undefined
+ : this.src || imgProps.src
+ }
alt={this.alt || imgProps.alt}
aria-label={this.alt || imgProps.alt}
onClick={this.click}
@@ -121,6 +149,7 @@ export default defineComponent({
style={[imgProps.style || '', { objectFit: this.objectFit }]}
data-error={this.showError}
data-preview-src={this.previewSrc || this.src}
+ data-src={this.src || imgProps.src}
/>
)
diff --git a/src/image/src/utils.ts b/src/image/src/utils.ts
new file mode 100644
index 00000000000..de7c35c88b7
--- /dev/null
+++ b/src/image/src/utils.ts
@@ -0,0 +1,42 @@
+let imgObserver: IntersectionObserver | null = null
+
+let imgObserverOptions: {
+ root: HTMLElement | null
+} | null
+
+const imgObserverCallback: IntersectionObserverCallback = (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ const img = entry.target as HTMLImageElement
+ if (!img.src) {
+ img.src = img.dataset.src || ''
+ }
+ }
+ })
+}
+
+export const imgObserverHandler: (
+ el: HTMLImageElement | null,
+ root?: string
+) => void = (el, root = 'body') => {
+ if (el === null) return
+ if (imgObserver === null) {
+ imgObserverOptions = {
+ root: document.querySelector(root)
+ }
+ imgObserver = new IntersectionObserver(
+ imgObserverCallback,
+ imgObserverOptions
+ )
+ }
+ imgObserver.observe(el)
+}
+
+export const imgUnobserverHandler: (el: HTMLImageElement | null) => void = (
+ el
+) => {
+ if (el === null) return
+ if (imgObserver) {
+ imgObserver.unobserve(el)
+ }
+}