199 lines
4.2 KiB
Plaintext
199 lines
4.2 KiB
Plaintext
|
|
<template>
|
|||
|
|
<div class="page">
|
|||
|
|
<div class="controls">
|
|||
|
|
<div class="row">
|
|||
|
|
<button class="btn" @click="generate(2000)">生成 2000 项</button>
|
|||
|
|
<button class="btn" @click="generate(5000)">生成 5000 项</button>
|
|||
|
|
<button class="btn" @click="clearList">清空</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="row">
|
|||
|
|
<text class="stat">FPS: {{fpsDisplay}}</text>
|
|||
|
|
<text class="stat">平均帧(ms): {{avgFrameMsDisplay}}</text>
|
|||
|
|
<text class="stat">items: {{items.length}}</text>
|
|||
|
|
</div>
|
|||
|
|
<div class="hint">说明:向下快速滚动列表以测试渲染/滚动性能;nvue 将使用原生渲染。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<scroll-view class="list" :scroll-y="true" show-scrollbar="true" @scroll="onScroll">
|
|||
|
|
<div class="item" v-for="item in items" :key="item.id">
|
|||
|
|
<div class="thumb" :style="{backgroundColor: item.color}"></div>
|
|||
|
|
<div class="meta">
|
|||
|
|
<text class="title">Item #{{item.id}}</text>
|
|||
|
|
<text class="desc">这是第 {{item.id}} 个条目,用于增加每个 DOM 的复杂度以测试渲染成本。多行文本强制布局多次换行,模拟真实列表。</text>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</scroll-view>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
items: [],
|
|||
|
|
// FPS 测量
|
|||
|
|
measuring: false,
|
|||
|
|
fpsDisplay: 0,
|
|||
|
|
avgFrameMsDisplay: 0,
|
|||
|
|
|
|||
|
|
// 内部用
|
|||
|
|
_frames: [],
|
|||
|
|
_rafId: null,
|
|||
|
|
_idleTO: null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
generate(n) {
|
|||
|
|
// 生成带颜色的占位项(避免网络请求)
|
|||
|
|
const list = []
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
list.push({
|
|||
|
|
id: i + 1,
|
|||
|
|
color: this._randColor()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
this.items = list
|
|||
|
|
// 清除测量结果
|
|||
|
|
this.fpsDisplay = 0
|
|||
|
|
this.avgFrameMsDisplay = 0
|
|||
|
|
},
|
|||
|
|
clearList() {
|
|||
|
|
this.items = []
|
|||
|
|
this.fpsDisplay = 0
|
|||
|
|
this.avgFrameMsDisplay = 0
|
|||
|
|
},
|
|||
|
|
_randColor() {
|
|||
|
|
const h = Math.floor(Math.random() * 360)
|
|||
|
|
const s = 60 + Math.floor(Math.random() * 20)
|
|||
|
|
const l = 55 + Math.floor(Math.random() * 10)
|
|||
|
|
return `hsl(${h} ${s}% ${l}%)`
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 滚动回调(来自原生 nvue scroll-view)
|
|||
|
|
onScroll(e) {
|
|||
|
|
// 每次 scroll 事件触发时重置空闲计时器
|
|||
|
|
// 开始/继续测量 FPS
|
|||
|
|
if (!this.measuring) this._startMeasure()
|
|||
|
|
clearTimeout(this._idleTO)
|
|||
|
|
this._idleTO = setTimeout(() => {
|
|||
|
|
this._stopMeasure()
|
|||
|
|
}, 300)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
_startMeasure() {
|
|||
|
|
this.measuring = true
|
|||
|
|
this._frames = []
|
|||
|
|
const pushFrame = (t) => {
|
|||
|
|
this._frames.push(t)
|
|||
|
|
// 保持最近 120 帧
|
|||
|
|
if (this._frames.length > 120) this._frames.shift()
|
|||
|
|
this._rafId = requestAnimationFrame(pushFrame)
|
|||
|
|
}
|
|||
|
|
this._rafId = requestAnimationFrame(pushFrame)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
_stopMeasure() {
|
|||
|
|
this.measuring = false
|
|||
|
|
if (this._rafId) {
|
|||
|
|
cancelAnimationFrame(this._rafId)
|
|||
|
|
this._rafId = null
|
|||
|
|
}
|
|||
|
|
// 计算 FPS 与平均帧时长
|
|||
|
|
if (this._frames.length < 2) {
|
|||
|
|
this.fpsDisplay = 0
|
|||
|
|
this.avgFrameMsDisplay = 0
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const intervals = []
|
|||
|
|
for (let i = 1; i < this._frames.length; i++) {
|
|||
|
|
intervals.push(this._frames[i] - this._frames[i - 1])
|
|||
|
|
}
|
|||
|
|
const sum = intervals.reduce((a, b) => a + b, 0)
|
|||
|
|
const avg = sum / intervals.length
|
|||
|
|
const fps = 1000 / avg
|
|||
|
|
this.avgFrameMsDisplay = avg.toFixed(2)
|
|||
|
|
this.fpsDisplay = Math.round(fps)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.page {
|
|||
|
|
flex: 1;
|
|||
|
|
background-color: #f5f6fa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.controls {
|
|||
|
|
padding: 20px;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.row {
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
padding: 10px 14px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
background: #2d8cf0;
|
|||
|
|
color: #fff;
|
|||
|
|
margin-right: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat {
|
|||
|
|
margin-right: 14px;
|
|||
|
|
font-size: 28px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hint {
|
|||
|
|
color: #888;
|
|||
|
|
font-size: 24px;
|
|||
|
|
margin-top: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.list {
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item {
|
|||
|
|
height: 160px;
|
|||
|
|
padding: 12px;
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: center;
|
|||
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.thumb {
|
|||
|
|
width: 120px;
|
|||
|
|
height: 120px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
margin-right: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meta {
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
font-size: 30px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.desc {
|
|||
|
|
font-size: 24px;
|
|||
|
|
color: #666;
|
|||
|
|
line-height: 32px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ensure full-height page in nvue */
|
|||
|
|
page,
|
|||
|
|
body,
|
|||
|
|
.page {
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
</style>
|