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
This commit is contained in:
sanbuphy
2026-02-01 23:42:12 +08:00
parent a9a5c5c8a7
commit ad95658a11
171 changed files with 16366 additions and 7946 deletions
@@ -14,7 +14,10 @@
v-for="strategy in strategies"
:key="strategy.name"
@click="selectStrategy(strategy)"
:class="['strategy-btn', { active: selectedStrategy.name === strategy.name }]"
:class="[
'strategy-btn',
{ active: selectedStrategy.name === strategy.name }
]"
>
<span class="strategy-icon">{{ strategy.icon }}</span>
<span class="strategy-name">{{ strategy.name }}</span>
@@ -43,11 +46,18 @@
<h2>{{ selectedStrategy.pageTitle }}</h2>
</div>
<div class="page-body">
<div class="resource-item" v-for="(resource, index) in selectedStrategy.resources" :key="index">
<div
class="resource-item"
v-for="(resource, index) in selectedStrategy.resources"
:key="index"
>
<div class="resource-icon">{{ resource.icon }}</div>
<div class="resource-info">
<div class="resource-name">{{ resource.name }}</div>
<div class="resource-status" :class="resource.cached ? 'cached' : 'network'">
<div
class="resource-status"
:class="resource.cached ? 'cached' : 'network'"
>
{{ resource.cached ? '✓ 来自缓存' : '↓ 从服务器下载' }}
</div>
</div>
@@ -68,7 +78,10 @@
<div class="metric-value" :class="selectedStrategy.performanceClass">
{{ selectedStrategy.loadTime }}
</div>
<div class="metric-change" :class="{ positive: selectedStrategy.isFast }">
<div
class="metric-change"
:class="{ positive: selectedStrategy.isFast }"
>
{{ selectedStrategy.compared }}
</div>
</div>
@@ -78,11 +91,12 @@
<span class="metric-icon">💾</span>
<span class="metric-title">缓存命中</span>
</div>
<div class="metric-value">
{{ selectedStrategy.cacheHit }}%
</div>
<div class="metric-value">{{ selectedStrategy.cacheHit }}%</div>
<div class="metric-bar">
<div class="metric-fill" :style="{ width: selectedStrategy.cacheHit + '%' }"></div>
<div
class="metric-fill"
:style="{ width: selectedStrategy.cacheHit + '%' }"
></div>
</div>
</div>
@@ -122,8 +136,14 @@
</tr>
</thead>
<tbody>
<tr v-for="strategy in strategies" :key="strategy.name" :class="{ highlighted: selectedStrategy.name === strategy.name }">
<td><strong>{{ strategy.name }}</strong></td>
<tr
v-for="strategy in strategies"
:key="strategy.name"
:class="{ highlighted: selectedStrategy.name === strategy.name }"
>
<td>
<strong>{{ strategy.name }}</strong>
</td>
<td>{{ strategy.speed }}</td>
<td>{{ strategy.updateDifficulty }}</td>
<td>{{ strategy.useCase }}</td>
@@ -148,10 +168,34 @@ const strategies = [
url: 'https://example.com/',
pageTitle: '页面加载缓慢',
resources: [
{ icon: '📄', name: 'index.html', size: '5 KB', time: '200ms', cached: false },
{ icon: '🎨', name: 'style.css', size: '50 KB', time: '300ms', cached: false },
{ icon: '⚙️', name: 'app.js', size: '200 KB', time: '800ms', cached: false },
{ icon: '🖼️', name: 'image.jpg', size: '150 KB', time: '500ms', cached: false }
{
icon: '📄',
name: 'index.html',
size: '5 KB',
time: '200ms',
cached: false
},
{
icon: '🎨',
name: 'style.css',
size: '50 KB',
time: '300ms',
cached: false
},
{
icon: '⚙️',
name: 'app.js',
size: '200 KB',
time: '800ms',
cached: false
},
{
icon: '🖼️',
name: 'image.jpg',
size: '150 KB',
time: '500ms',
cached: false
}
],
loadTime: '1.8s',
performanceClass: 'poor',
@@ -160,7 +204,8 @@ const strategies = [
cacheHit: 0,
requests: 4,
requestDesc: '所有资源都从网络下载',
description: '不使用任何缓存,每次访问都要重新下载所有资源。速度最慢,但内容总是最新的。',
description:
'不使用任何缓存,每次访问都要重新下载所有资源。速度最慢,但内容总是最新的。',
code: '# 禁用缓存\nCache-Control: no-cache',
speed: '慢',
updateDifficulty: '容易',
@@ -172,10 +217,34 @@ const strategies = [
url: 'https://example.com/',
pageTitle: '页面加载较快',
resources: [
{ icon: '📄', name: 'index.html', size: '5 KB', time: '50ms', cached: true },
{ icon: '🎨', name: 'style.css', size: '50 KB', time: '30ms', cached: true },
{ icon: '⚙️', name: 'app.js', size: '200 KB', time: '20ms', cached: true },
{ icon: '🖼️', name: 'image.jpg', size: '150 KB', time: '25ms', cached: true }
{
icon: '📄',
name: 'index.html',
size: '5 KB',
time: '50ms',
cached: true
},
{
icon: '🎨',
name: 'style.css',
size: '50 KB',
time: '30ms',
cached: true
},
{
icon: '⚙️',
name: 'app.js',
size: '200 KB',
time: '20ms',
cached: true
},
{
icon: '🖼️',
name: 'image.jpg',
size: '150 KB',
time: '25ms',
cached: true
}
],
loadTime: '125ms',
performanceClass: 'good',
@@ -184,7 +253,8 @@ const strategies = [
cacheHit: 100,
requests: 0,
requestDesc: '所有资源都来自缓存',
description: '设置固定的过期时间(如 1 年)。速度极快,但更新内容需要用户清除缓存或强制刷新。',
description:
'设置固定的过期时间(如 1 年)。速度极快,但更新内容需要用户清除缓存或强制刷新。',
code: '# Nginx 配置\nlocation ~* \\.(js|css|jpg|png)$ {\n expires: 1y;\n add_header: Cache-Control: public;\n}',
speed: '极快',
updateDifficulty: '困难',
@@ -196,10 +266,34 @@ const strategies = [
url: 'https://example.com/',
pageTitle: '页面加载快',
resources: [
{ icon: '📄', name: 'index.html', size: '5 KB', time: '50ms', cached: true },
{ icon: '🎨', name: 'style.css', size: '50 KB', time: '30ms', cached: true },
{ icon: '⚙️', name: 'app.js', size: '200 KB', time: '350ms', cached: false },
{ icon: '🖼️', name: 'image.jpg', size: '150 KB', time: '25ms', cached: true }
{
icon: '📄',
name: 'index.html',
size: '5 KB',
time: '50ms',
cached: true
},
{
icon: '🎨',
name: 'style.css',
size: '50 KB',
time: '30ms',
cached: true
},
{
icon: '⚙️',
name: 'app.js',
size: '200 KB',
time: '350ms',
cached: false
},
{
icon: '🖼️',
name: 'image.jpg',
size: '150 KB',
time: '25ms',
cached: true
}
],
loadTime: '455ms',
performanceClass: 'medium',
@@ -208,7 +302,8 @@ const strategies = [
cacheHit: 75,
requests: 1,
requestDesc: '仅下载已更新的资源',
description: '使用 ETag 或 Last-Modified 进行验证。资源未改变时返回 304,资源改变时下载新内容。',
description:
'使用 ETag 或 Last-Modified 进行验证。资源未改变时返回 304,资源改变时下载新内容。',
code: '# Nginx 配置\nlocation / {\n etag on;\n add_header Cache-Control: must-revalidate;\n}',
speed: '快',
updateDifficulty: '容易',
@@ -220,10 +315,28 @@ const strategies = [
url: 'https://example.com/',
pageTitle: '页面极速加载',
resources: [
{ icon: '📄', name: 'index.html', size: '5 KB', time: '10ms', cached: true },
{ icon: '🎨', name: 'style.css', size: '50 KB', time: '5ms', cached: true },
{
icon: '📄',
name: 'index.html',
size: '5 KB',
time: '10ms',
cached: true
},
{
icon: '🎨',
name: 'style.css',
size: '50 KB',
time: '5ms',
cached: true
},
{ icon: '⚙️', name: 'app.js', size: '200 KB', time: '5ms', cached: true },
{ icon: '🖼️', name: 'image.jpg', size: '150 KB', time: '5ms', cached: true }
{
icon: '🖼️',
name: 'image.jpg',
size: '150 KB',
time: '5ms',
cached: true
}
],
loadTime: '25ms',
performanceClass: 'excellent',
@@ -232,8 +345,9 @@ const strategies = [
cacheHit: 100,
requests: 0,
requestDesc: '完全离线可用',
description: 'Service Worker 拦截网络请求,从缓存中返回资源。可实现离线访问和即时加载。',
code: '// 注册 Service Worker\nif (\'serviceWorker\' in navigator) {\n navigator.serviceWorker.register(\'/sw.js\');\n}\n\n// sw.js\ncaches.open(\'v1\').then(cache => {\n cache.addAll([\'/\', \'/style.css\', \'/app.js\']);\n});',
description:
'Service Worker 拦截网络请求,从缓存中返回资源。可实现离线访问和即时加载。',
code: "// 注册 Service Worker\nif ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('/sw.js');\n}\n\n// sw.js\ncaches.open('v1').then(cache => {\n cache.addAll(['/', '/style.css', '/app.js']);\n});",
speed: '极快',
updateDifficulty: '中等',
useCase: 'PWA 应用和关键资源'
@@ -416,7 +530,9 @@ onMounted(() => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.loading-text {
@@ -6,7 +6,9 @@
<div class="crp-demo">
<div class="header">
<div class="title">关键渲染路径 (Critical Rendering Path)</div>
<div class="subtitle">浏览器如何将 HTMLCSS JavaScript 转换为像素</div>
<div class="subtitle">
浏览器如何将 HTMLCSS JavaScript 转换为像素
</div>
</div>
<div class="demo-container">
@@ -152,7 +154,9 @@
}"
@click="setStep(step.name)"
>
<div class="segment-label" :style="{ color: step.color }">{{ step.label }}</div>
<div class="segment-label" :style="{ color: step.color }">
{{ step.label }}
</div>
</div>
</div>
<div class="timeline-scale">
@@ -182,7 +186,10 @@
<div class="tip-icon"></div>
<div class="tip-content">
<h4>优化 JavaScript</h4>
<p>JS 会阻塞 DOM 构建使用 <code>defer</code> <code>async</code> 属性</p>
<p>
JS 会阻塞 DOM 构建使用 <code>defer</code>
<code>async</code> 属性
</p>
</div>
</div>
@@ -190,7 +197,10 @@
<div class="tip-icon">📐</div>
<div class="tip-content">
<h4>减少重排</h4>
<p>批量修改样式避免逐帧操作使用 <code>transform</code> 代替位置属性</p>
<p>
批量修改样式避免逐帧操作使用
<code>transform</code> 代替位置属性
</p>
</div>
</div>
</div>
@@ -205,10 +215,22 @@ const currentStep = ref('dom')
const timelineSteps = [
{ name: 'dom', label: 'DOM', start: 0, width: 20, color: '#3b82f6' },
{ name: 'cssom', label: 'CSSOM', start: 20, width: 15, color: '#8b5cf6' },
{ name: 'render', label: 'Render Tree', start: 35, width: 10, color: '#ec4899' },
{
name: 'render',
label: 'Render Tree',
start: 35,
width: 10,
color: '#ec4899'
},
{ name: 'layout', label: 'Layout', start: 45, width: 15, color: '#f59e0b' },
{ name: 'paint', label: 'Paint', start: 60, width: 20, color: '#10b981' },
{ name: 'composite', label: 'Composite', start: 80, width: 20, color: '#06b6d4' }
{
name: 'composite',
label: 'Composite',
start: 80,
width: 20,
color: '#06b6d4'
}
]
const totalDuration = computed(() => {
@@ -19,7 +19,9 @@
>
<div class="format-header">
<div class="format-name">{{ format.name }}</div>
<div class="format-badge" :class="format.badgeClass">{{ format.badge }}</div>
<div class="format-badge" :class="format.badgeClass">
{{ format.badge }}
</div>
</div>
<div class="format-preview" :style="{ background: format.gradient }">
@@ -41,7 +43,10 @@
<div class="metric">
<span class="metric-label">质量</span>
<div class="quality-bar">
<div class="quality-fill" :style="{ width: format.quality + '%' }"></div>
<div
class="quality-fill"
:style="{ width: format.quality + '%' }"
></div>
</div>
</div>
<div class="metric">
@@ -72,14 +77,19 @@
</thead>
<tbody>
<tr v-for="format in formats" :key="format.name">
<td><strong>{{ format.name }}</strong></td>
<td>
<strong>{{ format.name }}</strong>
</td>
<td>{{ format.sizeLevel }}</td>
<td>{{ format.qualityLevel }}</td>
<td>{{ format.transparency ? '✓' : '✗' }}</td>
<td>{{ format.animation ? '✓' : '✗' }}</td>
<td>
<div class="recommendation">
<div class="stars">{{ '★'.repeat(Math.round(format.rating)) }}{{ '☆'.repeat(5 - Math.round(format.rating)) }}</div>
<div class="stars">
{{ '★'.repeat(Math.round(format.rating))
}}{{ '☆'.repeat(5 - Math.round(format.rating)) }}
</div>
</div>
</td>
</tr>
@@ -95,7 +105,10 @@
<ul>
<li>优先使用 WebP 格式可减少 30-50% 的大小</li>
<li>为旧浏览器提供 JPEG/PNG 降级方案</li>
<li>使用 <code class="inline-code">&lt;picture&gt;</code> 元素实现自动降级</li>
<li>
使用
<code class="inline-code">&lt;picture&gt;</code> 元素实现自动降级
</li>
<li>照片使用 JPEG图标使用 PNG SVG</li>
</ul>
</div>
@@ -42,7 +42,11 @@
</div>
</div>
<div class="scroll-container" ref="scrollContainer" @scroll="handleScroll">
<div
class="scroll-container"
ref="scrollContainer"
@scroll="handleScroll"
>
<div class="content-area">
<div class="placeholder">向下滚动查看更多内容</div>
@@ -50,10 +54,16 @@
v-for="(image, index) in images"
:key="index"
class="image-item"
:ref="el => setImageRef(el, index)"
: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="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>
@@ -75,17 +85,27 @@
<div class="explanation">
<div class="explanation-item">
<h4>💡 懒加载原理</h4>
<p>只有当图片进入视口用户可见区域时才开始加载使用 Intersection Observer API 可以高效实现</p>
<p>
只有当图片进入视口用户可见区域时才开始加载使用 Intersection
Observer API 可以高效实现
</p>
</div>
<div class="explanation-item">
<h4>📊 性能收益</h4>
<p>懒加载可以节省 30-60% 的带宽大幅提升首屏加载速度特别是在移动端效果显著</p>
<p>
懒加载可以节省 30-60%
的带宽大幅提升首屏加载速度特别是在移动端效果显著
</p>
</div>
<div class="explanation-item">
<h4>🔧 实现方式</h4>
<p><code>loading="lazy"</code> 属性是最简单的方式现代浏览器都支持需要更多控制时使用 Intersection Observer</p>
<p>
<code>loading="lazy"</code>
属性是最简单的方式现代浏览器都支持需要更多控制时使用
Intersection Observer
</p>
</div>
</div>
</div>
@@ -103,12 +123,15 @@ const imageRefs = ref([])
const images = ref([])
const loadedImages = computed(() => {
return images.value.filter(img => img.loaded).length
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
const notLoaded = images.value.filter(
(img) =>
!img.loaded && !imageRefs.value[images.value.indexOf(img)]?.isVisible
).length
return notLoaded * 150 // 假设每张图片 150KB
})
@@ -167,10 +190,13 @@ function loadImage(index) {
image.loading = true
// 模拟加载延迟
setTimeout(() => {
image.loaded = true
image.loading = false
}, 300 + Math.random() * 500)
setTimeout(
() => {
image.loaded = true
image.loading = false
},
300 + Math.random() * 500
)
}
watch(mode, () => {
@@ -342,7 +368,9 @@ onMounted(() => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.placeholder-text {
@@ -13,7 +13,13 @@
<label>
模拟加载时间<strong>{{ loadTime }}</strong>
</label>
<input v-model.number="loadTime" type="range" min="0.5" max="5" step="0.1" />
<input
v-model.number="loadTime"
type="range"
min="0.5"
max="5"
step="0.1"
/>
<button @click="startLoading" :disabled="isLoading">
{{ isLoading ? '加载中...' : '模拟加载' }}
</button>
@@ -69,10 +75,19 @@
<div class="section">
<h4>指标说明</h4>
<ul>
<li><strong>FCP</strong>浏览器首次绘制内容的时间用户第一次看到页面有内容</li>
<li>
<strong>FCP</strong
>浏览器首次绘制内容的时间用户第一次看到页面有内容
</li>
<li><strong>LCP</strong>最大内容绘制完成的时间主要内容可见</li>
<li><strong>FID</strong>用户首次交互到浏览器响应的时间页面是否可交互</li>
<li><strong>CLS</strong>页面布局在加载过程中的稳定性是否发生意外跳动</li>
<li>
<strong>FID</strong
>用户首次交互到浏览器响应的时间页面是否可交互
</li>
<li>
<strong>CLS</strong
>页面布局在加载过程中的稳定性是否发生意外跳动
</li>
</ul>
</div>
@@ -106,7 +121,9 @@ const isLoading = ref(false)
const fcp = computed(() => (loadTime.value * 0.3).toFixed(1))
const lcp = computed(() => (loadTime.value * 0.7).toFixed(1))
const fid = computed(() => Math.round(loadTime.value * 80))
const cls = computed(() => (loadTime.value > 3 ? '0.25' : loadTime.value > 2 ? '0.15' : '0.05'))
const cls = computed(() =>
loadTime.value > 3 ? '0.25' : loadTime.value > 2 ? '0.15' : '0.05'
)
const fcpStatus = computed(() => {
const value = parseFloat(fcp.value)
@@ -228,17 +245,29 @@ function startLoading() {
.metric-card.good {
border-color: #22c55e;
background: linear-gradient(135deg, var(--vp-c-bg) 0%, rgba(34, 197, 94, 0.05) 100%);
background: linear-gradient(
135deg,
var(--vp-c-bg) 0%,
rgba(34, 197, 94, 0.05) 100%
);
}
.metric-card.needs-improvement {
border-color: #f59e0b;
background: linear-gradient(135deg, var(--vp-c-bg) 0%, rgba(245, 158, 11, 0.05) 100%);
background: linear-gradient(
135deg,
var(--vp-c-bg) 0%,
rgba(245, 158, 11, 0.05) 100%
);
}
.metric-card.poor {
border-color: #ef4444;
background: linear-gradient(135deg, var(--vp-c-bg) 0%, rgba(239, 68, 68, 0.05) 100%);
background: linear-gradient(
135deg,
var(--vp-c-bg) 0%,
rgba(239, 68, 68, 0.05) 100%
);
}
.metric-header {
@@ -27,7 +27,11 @@
<div class="performance-meter">
<div class="meter-label">性能影响</div>
<div class="meter-bar">
<div class="meter-fill" :class="performanceLevel.class" :style="{ width: performanceImpact + '%' }"></div>
<div
class="meter-fill"
:class="performanceLevel.class"
:style="{ width: performanceImpact + '%' }"
></div>
</div>
<div class="meter-value" :class="performanceLevel.class">
{{ performanceLevel.text }}
@@ -59,16 +63,24 @@
<h4>重绘操作 (Repaint)</h4>
<p class="control-desc">只改变外观不触发布局</p>
<button @click="changeColor" class="btn repaint">改变颜色</button>
<button @click="changeBackground" class="btn repaint">改变背景</button>
<button @click="changeBackground" class="btn repaint">
改变背景
</button>
<button @click="toggleBorder" class="btn repaint">切换边框</button>
</div>
<div class="control-section">
<h4>合成操作 (Composite)</h4>
<p class="control-desc">只触发合成性能最佳</p>
<button @click="transformTranslate" class="btn composite">Transform 位移</button>
<button @click="transformRotate" class="btn composite">Transform 旋转</button>
<button @click="changeOpacity" class="btn composite">改变透明度</button>
<button @click="transformTranslate" class="btn composite">
Transform 位移
</button>
<button @click="transformRotate" class="btn composite">
Transform 旋转
</button>
<button @click="changeOpacity" class="btn composite">
改变透明度
</button>
</div>
</div>
</div>
@@ -76,17 +88,24 @@
<div class="info-section">
<div class="info-card">
<h4>什么是重排 (Reflow)</h4>
<p>当元素的位置尺寸发生变化时浏览器需要重新计算布局这个过程叫重排重排开销最大因为要重新计算所有受影响元素的位置</p>
<p>
当元素的位置尺寸发生变化时浏览器需要重新计算布局这个过程叫重排重排开销最大因为要重新计算所有受影响元素的位置
</p>
</div>
<div class="info-card">
<h4>什么是重绘 (Repaint)</h4>
<p>当元素的外观颜色背景发生变化但位置不变时浏览器只需要重新绘制像素这个过程叫重绘比重排快但仍有开销</p>
<p>
当元素的外观颜色背景发生变化但位置不变时浏览器只需要重新绘制像素这个过程叫重绘比重排快但仍有开销
</p>
</div>
<div class="info-card">
<h4>什么是合成 (Composite)</h4>
<p>使用 transform opacity 等属性浏览器可以在合成层上完成变化完全不触发布局和绘制性能最佳推荐优先使用</p>
<p>
使用 transform opacity
等属性浏览器可以在合成层上完成变化完全不触发布局和绘制性能最佳推荐优先使用
</p>
</div>
</div>
</div>
@@ -96,9 +115,45 @@
import { ref, computed } from 'vue'
const boxes = ref([
{ id: 1, x: 0, y: 0, width: 80, height: 80, color: '#3b82f6', bg: '#dbeafe', rotation: 0, opacity: 1, border: false, selected: false },
{ id: 2, x: 100, y: 0, width: 80, height: 80, color: '#8b5cf6', bg: '#ede9fe', rotation: 0, opacity: 1, border: false, selected: false },
{ id: 3, x: 0, y: 100, width: 80, height: 80, color: '#ec4899', bg: '#fce7f3', rotation: 0, opacity: 1, border: false, selected: false }
{
id: 1,
x: 0,
y: 0,
width: 80,
height: 80,
color: '#3b82f6',
bg: '#dbeafe',
rotation: 0,
opacity: 1,
border: false,
selected: false
},
{
id: 2,
x: 100,
y: 0,
width: 80,
height: 80,
color: '#8b5cf6',
bg: '#ede9fe',
rotation: 0,
opacity: 1,
border: false,
selected: false
},
{
id: 3,
x: 0,
y: 100,
width: 80,
height: 80,
color: '#ec4899',
bg: '#fce7f3',
rotation: 0,
opacity: 1,
border: false,
selected: false
}
])
const currentOperation = ref('无')
@@ -131,7 +186,7 @@ function getBoxStyle(box) {
}
function selectBox(id) {
boxes.value.forEach(b => b.selected = b.id === id)
boxes.value.forEach((b) => (b.selected = b.id === id))
}
function updateMetrics(operation, impact, affected) {
@@ -141,14 +196,14 @@ function updateMetrics(operation, impact, affected) {
}
function changeWidth() {
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.width = 60 + Math.random() * 60
})
updateMetrics('改变宽度', 90, boxes.value.length)
}
function changePosition() {
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.x = Math.random() * 150
box.y = Math.random() * 150
})
@@ -175,7 +230,7 @@ function addBox() {
function changeColor() {
const colors = ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b']
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.color = colors[Math.floor(Math.random() * colors.length)]
})
updateMetrics('改变颜色', 50, boxes.value.length)
@@ -183,35 +238,35 @@ function changeColor() {
function changeBackground() {
const bgs = ['#dbeafe', '#ede9fe', '#fce7f3', '#d1fae5', '#fef3c7']
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.bg = bgs[Math.floor(Math.random() * bgs.length)]
})
updateMetrics('改变背景', 45, boxes.value.length)
}
function toggleBorder() {
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.border = !box.border
})
updateMetrics('切换边框', 55, boxes.value.length)
}
function transformTranslate() {
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.x += Math.random() * 20 - 10
})
updateMetrics('Transform 位移', 10, boxes.value.length)
}
function transformRotate() {
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.rotation += Math.random() * 30 - 15
})
updateMetrics('Transform 旋转', 10, boxes.value.length)
}
function changeOpacity() {
boxes.value.forEach(box => {
boxes.value.forEach((box) => {
box.opacity = 0.5 + Math.random() * 0.5
})
updateMetrics('改变透明度', 10, boxes.value.length)
@@ -16,9 +16,14 @@ const containerRef = ref(null)
// Virtual scrolling calculations
const startIndex = computed(() => Math.floor(scrollTop.value / ITEM_HEIGHT))
const endIndex = computed(() => Math.min(TOTAL_ITEMS, startIndex.value + Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + 2))
const endIndex = computed(() =>
Math.min(
TOTAL_ITEMS,
startIndex.value + Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + 2
)
)
const visibleItems = computed(() => {
return items.slice(startIndex.value, endIndex.value).map(item => ({
return items.slice(startIndex.value, endIndex.value).map((item) => ({
...item,
top: item.id * ITEM_HEIGHT
}))
@@ -47,23 +52,28 @@ const renderedCount = computed(() => visibleItems.value.length)
</div>
<div class="stat-box">
<div class="stat-label">Memory Saved</div>
<div class="stat-value">~{{ ((1 - renderedCount / TOTAL_ITEMS) * 100).toFixed(1) }}%</div>
<div class="stat-value">
~{{ ((1 - renderedCount / TOTAL_ITEMS) * 100).toFixed(1) }}%
</div>
</div>
</div>
<div
class="scroll-container"
<div
class="scroll-container"
ref="containerRef"
@scroll="onScroll"
:style="{ height: CONTAINER_HEIGHT + 'px' }"
>
<div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
<div class="visible-list">
<div
v-for="item in visibleItems"
<div
v-for="item in visibleItems"
:key="item.id"
class="list-item"
:style="{ transform: `translateY(${item.top}px)`, height: ITEM_HEIGHT + 'px' }"
:style="{
transform: `translateY(${item.top}px)`,
height: ITEM_HEIGHT + 'px'
}"
>
<span class="item-index">{{ item.id + 1 }}</span>
<span class="item-content">{{ item.content }}</span>
@@ -73,10 +83,11 @@ const renderedCount = computed(() => visibleItems.value.length)
<div class="explanation">
<p>
<strong>How it works:</strong> Instead of rendering all {{ TOTAL_ITEMS }} items at once,
we only render the items currently visible in the viewport (plus a small buffer).
As you scroll, we calculate which items should be visible and position them absolutely
to create the illusion of a full list.
<strong>How it works:</strong> Instead of rendering all
{{ TOTAL_ITEMS }} items at once, we only render the items currently
visible in the viewport (plus a small buffer). As you scroll, we
calculate which items should be visible and position them absolutely to
create the illusion of a full list.
</p>
</div>
</div>