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:
+147
-31
@@ -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 {
|
||||
|
||||
+28
-6
@@ -6,7 +6,9 @@
|
||||
<div class="crp-demo">
|
||||
<div class="header">
|
||||
<div class="title">关键渲染路径 (Critical Rendering Path)</div>
|
||||
<div class="subtitle">浏览器如何将 HTML、CSS 和 JavaScript 转换为像素</div>
|
||||
<div class="subtitle">
|
||||
浏览器如何将 HTML、CSS 和 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(() => {
|
||||
|
||||
+18
-5
@@ -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"><picture></code> 元素实现自动降级</li>
|
||||
<li>
|
||||
使用
|
||||
<code class="inline-code"><picture></code> 元素实现自动降级
|
||||
</li>
|
||||
<li>照片使用 JPEG,图标使用 PNG 或 SVG</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
+42
-14
@@ -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 {
|
||||
|
||||
+37
-8
@@ -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 {
|
||||
|
||||
+75
-20
@@ -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)
|
||||
|
||||
+23
-12
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user