Files
test-repo/docs/.vitepress/theme/components/appendix/frontend-performance/LazyLoadingDemo.vue
T
sanbuphy ad95658a11 feat(docs): add NavGrid/NavCard components and restructure stage pages
- Add NavGrid.vue and NavCard.vue components for better navigation layout
- Restructure stage-0 index pages across languages into intro.md with new navigation components
- Remove old stage-0 index.md files and update stage-3 pages similarly
- Add new dependencies 'claude' and 'codex' to package.json
- Improve code formatting in multiple Vue components for better readability
- Update documentation content and structure for better user experience
2026-02-01 23:42:12 +08:00

452 lines
9.3 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
LazyLoadingDemo.vue
懒加载演示
-->
<template>
<div class="lazy-loading-demo">
<div class="header">
<div class="title">图片懒加载节省带宽提升性能</div>
<div class="subtitle">对比懒加载和立即加载的区别</div>
</div>
<div class="demo-container">
<div class="mode-selector">
<button
@click="mode = 'eager'"
:class="['mode-btn', { active: mode === 'eager' }]"
>
📦 立即加载
</button>
<button
@click="mode = 'lazy'"
:class="['mode-btn', { active: mode === 'lazy' }]"
>
懒加载
</button>
</div>
<div class="stats-bar">
<div class="stat">
<span class="stat-label">已加载图片</span>
<span class="stat-value">{{ loadedImages }} / {{ totalImages }}</span>
</div>
<div class="stat">
<span class="stat-label">节省流量</span>
<span class="stat-value" :class="{ positive: savedBandwidth > 0 }">
{{ savedBandwidth > 0 ? '-' : '' }}{{ savedBandwidth }} KB
</span>
</div>
<div class="stat">
<span class="stat-label">加载时间</span>
<span class="stat-value">{{ loadTime }} ms</span>
</div>
</div>
<div
class="scroll-container"
ref="scrollContainer"
@scroll="handleScroll"
>
<div class="content-area">
<div class="placeholder">向下滚动查看更多内容</div>
<div
v-for="(image, index) in images"
:key="index"
class="image-item"
:ref="(el) => setImageRef(el, index)"
>
<div
class="image-wrapper"
:class="{ loading: image.loading, loaded: image.loaded }"
>
<div
v-if="!image.loaded && mode === 'lazy'"
class="placeholder-box"
>
<div class="spinner"></div>
<div class="placeholder-text">加载中...</div>
</div>
<div v-else-if="image.loaded" class="image-box">
<div class="image-icon">🖼</div>
<div class="image-info">
<div class="image-size">{{ image.size }}</div>
<div class="image-dim">{{ image.dimensions }}</div>
</div>
</div>
</div>
<div class="image-caption">图片 {{ index + 1 }}</div>
</div>
<div class="placeholder">已经到底了</div>
</div>
</div>
<div class="explanation">
<div class="explanation-item">
<h4>💡 懒加载原理</h4>
<p>
只有当图片进入视口用户可见区域时才开始加载使用 Intersection
Observer API 可以高效实现
</p>
</div>
<div class="explanation-item">
<h4>📊 性能收益</h4>
<p>
懒加载可以节省 30-60%
的带宽大幅提升首屏加载速度特别是在移动端效果显著
</p>
</div>
<div class="explanation-item">
<h4>🔧 实现方式</h4>
<p>
<code>loading="lazy"</code>
属性是最简单的方式现代浏览器都支持需要更多控制时使用
Intersection Observer
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
const mode = ref('eager')
const scrollContainer = ref(null)
const totalImages = 12
const imageRefs = ref([])
const images = ref([])
const loadedImages = computed(() => {
return images.value.filter((img) => img.loaded).length
})
const savedBandwidth = computed(() => {
if (mode.value === 'eager') return 0
const notLoaded = images.value.filter(
(img) =>
!img.loaded && !imageRefs.value[images.value.indexOf(img)]?.isVisible
).length
return notLoaded * 150 // 假设每张图片 150KB
})
const loadTime = computed(() => {
if (mode.value === 'eager') return 2400
return loadedImages.value * 150
})
function initializeImages() {
images.value = Array.from({ length: totalImages }, (_, i) => ({
loaded: mode.value === 'eager',
loading: false,
size: '150 KB',
dimensions: '800×600'
}))
}
function setImageRef(el, index) {
if (el) {
imageRefs.value[index] = el
}
}
function handleScroll() {
if (mode.value === 'lazy') {
checkVisibility()
}
}
function checkVisibility() {
const container = scrollContainer.value
if (!container) return
const containerRect = container.getBoundingClientRect()
const threshold = 100 // 提前 100px 开始加载
images.value.forEach((image, index) => {
if (image.loaded || image.loading) return
const ref = imageRefs.value[index]
if (!ref) return
const rect = ref.getBoundingClientRect()
const isVisible = rect.top < containerRect.bottom + threshold
if (isVisible) {
loadImage(index)
}
})
}
function loadImage(index) {
const image = images.value[index]
if (!image || image.loaded || image.loading) return
image.loading = true
// 模拟加载延迟
setTimeout(
() => {
image.loaded = true
image.loading = false
},
300 + Math.random() * 500
)
}
watch(mode, () => {
initializeImages()
if (mode.value === 'lazy') {
setTimeout(() => checkVisibility(), 100)
}
})
onMounted(() => {
initializeImages()
if (mode.value === 'lazy') {
setTimeout(() => checkVisibility(), 100)
}
})
</script>
<style scoped>
.lazy-loading-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.3rem;
}
.demo-container {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.5rem;
}
.mode-selector {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
justify-content: center;
}
.mode-btn {
padding: 0.6rem 1.2rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s;
color: var(--vp-c-text-1);
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: #fff;
}
.stats-bar {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.stat {
flex: 1;
min-width: 120px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.8rem;
text-align: center;
}
.stat-label {
display: block;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
}
.stat-value {
display: block;
font-size: 1.1rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.stat-value.positive {
color: #22c55e;
}
.scroll-container {
height: 400px;
overflow-y: auto;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1.5rem;
background: var(--vp-c-bg-soft);
}
.content-area {
padding: 1rem;
}
.placeholder {
text-align: center;
padding: 1.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.image-item {
margin-bottom: 1rem;
}
.image-wrapper {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
}
.image-wrapper.loading {
border-color: var(--vp-c-brand);
}
.image-wrapper.loaded {
border-color: #22c55e;
}
.placeholder-box {
height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--vp-c-bg-soft);
}
.spinner {
width: 30px;
height: 30px;
border: 3px solid var(--vp-c-divider);
border-top-color: var(--vp-c-brand);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.placeholder-text {
margin-top: 0.5rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.image-box {
height: 150px;
display: flex;
align-items: center;
padding: 1rem;
gap: 1rem;
}
.image-icon {
font-size: 3rem;
}
.image-info {
flex: 1;
}
.image-size {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.3rem;
}
.image-dim {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.image-caption {
text-align: center;
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.explanation {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.explanation-item {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
}
.explanation-item h4 {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.explanation-item p {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0;
}
.explanation-item code {
background: var(--vp-c-bg);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.8rem;
color: var(--vp-c-brand);
}
</style>