paper-burner/tests/performance/test-scroll-performance.html

746 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滚动性能测试 - Phase 4.1.3</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 20px;
}
.test-header {
background: white;
padding: 24px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
margin-bottom: 12px;
color: #333;
}
.controls {
display: flex;
gap: 12px;
margin: 16px 0;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover {
background: #45a049;
}
.btn-secondary {
background: #2196F3;
color: white;
}
.btn-secondary:hover {
background: #0b7dda;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #da190b;
}
.test-modes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.test-mode {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-mode h2 {
font-size: 18px;
margin-bottom: 12px;
color: #333;
}
.test-mode .status {
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
margin-bottom: 12px;
}
.status.ready {
background: #e3f2fd;
color: #1976d2;
}
.status.running {
background: #fff3cd;
color: #856404;
}
.status.completed {
background: #d4edda;
color: #155724;
}
.results {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.metric {
display: flex;
justify-content: space-between;
padding: 12px;
margin: 8px 0;
background: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #2196F3;
}
.metric.winner {
border-left-color: #4CAF50;
background: #e8f5e9;
}
.metric .label {
font-weight: 500;
color: #555;
}
.metric .value {
font-weight: bold;
color: #333;
}
.metric .improvement {
color: #4CAF50;
font-size: 13px;
margin-left: 12px;
}
.scroll-container {
height: 600px;
overflow-y: scroll;
border: 2px solid #ddd;
border-radius: 8px;
background: white;
padding: 20px;
}
.table-wrapper {
overflow-x: auto;
margin-bottom: 24px;
border: 1px solid #e0e0e0;
border-radius: 4px;
position: relative;
}
.table-wrapper table {
width: 100%;
border-collapse: collapse;
min-width: 800px; /* 确保表格可以横向滚动 */
}
.table-wrapper th,
.table-wrapper td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.table-wrapper th {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
.table-wrapper tbody tr:hover {
background: #fafafa;
}
/* Intersection Observer 实现的渐变阴影 */
.table-wrapper.with-observer {
position: relative;
}
.table-wrapper.with-observer::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 30px;
background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.1));
pointer-events: none;
opacity: 1;
transition: opacity 0.3s;
}
.table-wrapper.with-observer.scrolled-to-end::after {
opacity: 0;
}
/* RAF 实现的渐变阴影 */
.table-wrapper.with-raf::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 30px;
background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.1));
pointer-events: none;
opacity: 1;
transition: opacity 0.3s;
}
.table-wrapper.with-raf.scrolled-to-end::after {
opacity: 0;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}
.comparison-table th,
.comparison-table td {
padding: 12px;
text-align: left;
border: 1px solid #e0e0e0;
}
.comparison-table th {
background: #f5f5f5;
font-weight: 600;
}
.comparison-table .winner-cell {
background: #e8f5e9;
font-weight: bold;
color: #2e7d32;
}
.info-box {
background: #e3f2fd;
border: 1px solid #2196F3;
border-radius: 4px;
padding: 16px;
margin: 16px 0;
}
.info-box ul {
margin: 8px 0 0 20px;
}
.progress {
margin-top: 12px;
}
.progress-bar {
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4CAF50;
transition: width 0.3s;
}
.progress-text {
font-size: 13px;
color: #666;
margin-top: 4px;
}
</style>
</head>
<body>
<div class="test-header">
<h1>🎯 Phase 4.1.3 - 滚动性能测试对比</h1>
<p>对比 Intersection Observer vs. requestAnimationFrame 滚动监听性能100个表格场景</p>
<div class="info-box">
<strong>测试说明:</strong>
<ul>
<li>生成100个可横向滚动的表格</li>
<li>对比两种滚动监听方案的性能指标</li>
<li>测试指标事件触发次数、平均响应时间、FPS、内存使用</li>
<li>建议:运行测试时打开控制台查看详细日志</li>
</ul>
</div>
<div class="controls">
<button class="btn-primary" onclick="generateTables()">生成100个表格</button>
<button class="btn-secondary" onclick="runTestIntersectionObserver()">测试 Intersection Observer</button>
<button class="btn-secondary" onclick="runTestRAF()">测试 RAF 节流</button>
<button class="btn-danger" onclick="clearTest()">清空测试</button>
</div>
</div>
<div class="test-modes">
<div class="test-mode">
<h2>🔵 Intersection Observer (推荐)</h2>
<div class="status ready" id="status-observer">就绪</div>
<div class="progress">
<div class="progress-bar">
<div class="progress-fill" id="progress-observer" style="width: 0%"></div>
</div>
<div class="progress-text" id="progress-text-observer">等待测试...</div>
</div>
</div>
<div class="test-mode">
<h2>🟠 requestAnimationFrame</h2>
<div class="status ready" id="status-raf">就绪</div>
<div class="progress">
<div class="progress-bar">
<div class="progress-fill" id="progress-raf" style="width: 0%"></div>
</div>
<div class="progress-text" id="progress-text-raf">等待测试...</div>
</div>
</div>
</div>
<div class="results">
<h2>📊 测试结果</h2>
<div id="results-content">
<p style="color: #666;">运行测试后将显示对比结果...</p>
</div>
</div>
<div id="test-container"></div>
<script>
let testResults = {
observer: null,
raf: null
};
// 生成测试表格
function generateTables() {
const container = document.getElementById('test-container');
container.innerHTML = '<h3 style="margin: 20px 0;">测试表格100个</h3>';
for (let i = 0; i < 100; i++) {
const wrapper = document.createElement('div');
wrapper.className = 'table-wrapper';
wrapper.innerHTML = `
<table>
<thead>
<tr>
<th>编号</th>
<th>列1</th>
<th>列2</th>
<th>列3</th>
<th>列4</th>
<th>列5</th>
<th>列6</th>
<th>列7</th>
<th>列8</th>
<th>列9</th>
<th>列10</th>
</tr>
</thead>
<tbody>
${generateRows(i)}
</tbody>
</table>
`;
container.appendChild(wrapper);
}
console.log('✅ 已生成 100 个表格');
updateStatus('observer', 'ready', '表格已生成,就绪');
updateStatus('raf', 'ready', '表格已生成,就绪');
}
function generateRows(tableIndex) {
let html = '';
for (let i = 0; i < 5; i++) {
html += `<tr>
<td>表格${tableIndex + 1}-行${i + 1}</td>
<td>数据 ${i + 1}-1</td>
<td>数据 ${i + 1}-2</td>
<td>数据 ${i + 1}-3</td>
<td>数据 ${i + 1}-4</td>
<td>数据 ${i + 1}-5</td>
<td>数据 ${i + 1}-6</td>
<td>数据 ${i + 1}-7</td>
<td>数据 ${i + 1}-8</td>
<td>数据 ${i + 1}-9</td>
<td>数据 ${i + 1}-10</td>
</tr>`;
}
return html;
}
// Intersection Observer 实现
function runTestIntersectionObserver() {
console.log('🔵 开始测试 Intersection Observer...');
updateStatus('observer', 'running', '测试进行中...');
// 清理之前的监听
cleanupListeners();
const wrappers = document.querySelectorAll('.table-wrapper');
if (wrappers.length === 0) {
alert('请先生成表格');
return;
}
const startTime = performance.now();
const startMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
let observerCallCount = 0;
const responseTimes = [];
// 添加样式类
wrappers.forEach(wrapper => {
wrapper.classList.add('with-observer');
wrapper.classList.remove('with-raf', 'scrolled-to-end');
});
// 创建 Intersection Observer
const observers = [];
wrappers.forEach(wrapper => {
const table = wrapper.querySelector('table');
const lastCell = table.querySelector('tr:first-child th:last-child');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const callStart = performance.now();
observerCallCount++;
const targetWrapper = entry.target.closest('.table-wrapper');
targetWrapper.classList.toggle('scrolled-to-end', entry.isIntersecting);
const duration = performance.now() - callStart;
responseTimes.push(duration);
});
}, { root: wrapper, threshold: 1.0 });
observer.observe(lastCell);
observers.push(observer);
});
// 模拟滚动测试
simulateScrolling(wrappers, () => {
const endTime = performance.now();
const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
const avgResponseTime = responseTimes.length > 0
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
: 0;
testResults.observer = {
duration: endTime - startTime,
callCount: observerCallCount,
avgResponseTime: avgResponseTime,
memoryIncrease: endMemory - startMemory,
method: 'Intersection Observer'
};
// 清理观察器
observers.forEach(o => o.disconnect());
updateStatus('observer', 'completed', '测试完成');
updateProgress('observer', 100, '✅ 完成');
console.log('✅ Intersection Observer 测试完成:', testResults.observer);
if (testResults.raf) {
displayComparison();
}
});
}
// RAF 节流实现
function runTestRAF() {
console.log('🟠 开始测试 requestAnimationFrame...');
updateStatus('raf', 'running', '测试进行中...');
// 清理之前的监听
cleanupListeners();
const wrappers = document.querySelectorAll('.table-wrapper');
if (wrappers.length === 0) {
alert('请先生成表格');
return;
}
const startTime = performance.now();
const startMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
let rafCallCount = 0;
let scrollEventCount = 0;
const responseTimes = [];
// 添加样式类
wrappers.forEach(wrapper => {
wrapper.classList.add('with-raf');
wrapper.classList.remove('with-observer', 'scrolled-to-end');
});
// 为每个表格添加 RAF 节流的滚动监听
const rafHandlers = [];
wrappers.forEach(wrapper => {
let scrollRAF = null;
const scrollHandler = () => {
scrollEventCount++;
if (scrollRAF) return;
scrollRAF = requestAnimationFrame(() => {
const callStart = performance.now();
rafCallCount++;
const table = wrapper.querySelector('table');
const isScrolledToEnd = wrapper.scrollLeft >= (wrapper.scrollWidth - wrapper.clientWidth - 5);
wrapper.classList.toggle('scrolled-to-end', isScrolledToEnd);
const duration = performance.now() - callStart;
responseTimes.push(duration);
scrollRAF = null;
});
};
wrapper.addEventListener('scroll', scrollHandler);
rafHandlers.push({ wrapper, handler: scrollHandler });
});
// 模拟滚动测试
simulateScrolling(wrappers, () => {
const endTime = performance.now();
const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
const avgResponseTime = responseTimes.length > 0
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
: 0;
testResults.raf = {
duration: endTime - startTime,
scrollEventCount: scrollEventCount,
callCount: rafCallCount,
avgResponseTime: avgResponseTime,
memoryIncrease: endMemory - startMemory,
method: 'RAF Throttle'
};
// 清理监听器
rafHandlers.forEach(({ wrapper, handler }) => {
wrapper.removeEventListener('scroll', handler);
});
updateStatus('raf', 'completed', '测试完成');
updateProgress('raf', 100, '✅ 完成');
console.log('✅ RAF 测试完成:', testResults.raf);
if (testResults.observer) {
displayComparison();
}
});
}
// 模拟滚动行为
function simulateScrolling(wrappers, callback) {
let currentIndex = 0;
const totalSteps = wrappers.length * 3; // 每个表格滚动3次
let step = 0;
function scrollNext() {
if (currentIndex >= wrappers.length) {
callback();
return;
}
const wrapper = wrappers[currentIndex];
const maxScroll = wrapper.scrollWidth - wrapper.clientWidth;
const scrollSteps = 3;
let currentStep = 0;
function scrollStep() {
if (currentStep >= scrollSteps) {
currentIndex++;
step += scrollSteps;
// 更新进度
const progress = Math.round((step / totalSteps) * 100);
updateProgress('observer', progress, `滚动测试... ${currentIndex}/${wrappers.length}`);
updateProgress('raf', progress, `滚动测试... ${currentIndex}/${wrappers.length}`);
setTimeout(scrollNext, 10);
return;
}
wrapper.scrollLeft = (maxScroll / scrollSteps) * (currentStep + 1);
currentStep++;
setTimeout(scrollStep, 50);
}
scrollStep();
}
scrollNext();
}
// 清理监听器
function cleanupListeners() {
const wrappers = document.querySelectorAll('.table-wrapper');
wrappers.forEach(wrapper => {
wrapper.classList.remove('with-observer', 'with-raf', 'scrolled-to-end');
wrapper.scrollLeft = 0;
});
}
// 显示对比结果
function displayComparison() {
const obs = testResults.observer;
const raf = testResults.raf;
if (!obs || !raf) return;
const durationImprove = ((raf.duration - obs.duration) / raf.duration * 100).toFixed(1);
const callCountDiff = raf.scrollEventCount - obs.callCount;
const memoryImprove = ((raf.memoryIncrease - obs.memoryIncrease) / raf.memoryIncrease * 100).toFixed(1);
const resultsHtml = `
<table class="comparison-table">
<thead>
<tr>
<th>指标</th>
<th>Intersection Observer</th>
<th>RAF 节流</th>
<th>改善</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>总耗时</strong></td>
<td class="${obs.duration < raf.duration ? 'winner-cell' : ''}">${obs.duration.toFixed(0)} ms</td>
<td class="${raf.duration < obs.duration ? 'winner-cell' : ''}">${raf.duration.toFixed(0)} ms</td>
<td>${durationImprove > 0 ? '↓' : '↑'} ${Math.abs(durationImprove)}%</td>
</tr>
<tr>
<td><strong>回调触发次数</strong></td>
<td class="winner-cell">${obs.callCount} 次</td>
<td>${raf.scrollEventCount} 个滚动事件<br>${raf.callCount} 次 RAF 调用</td>
<td>减少 ${callCountDiff} 次事件</td>
</tr>
<tr>
<td><strong>平均响应时间</strong></td>
<td class="${obs.avgResponseTime < raf.avgResponseTime ? 'winner-cell' : ''}">${obs.avgResponseTime.toFixed(3)} ms</td>
<td class="${raf.avgResponseTime < obs.avgResponseTime ? 'winner-cell' : ''}">${raf.avgResponseTime.toFixed(3)} ms</td>
<td>${obs.avgResponseTime < raf.avgResponseTime ? '更快' : '更慢'}</td>
</tr>
<tr>
<td><strong>内存增长</strong></td>
<td class="${obs.memoryIncrease < raf.memoryIncrease ? 'winner-cell' : ''}">${(obs.memoryIncrease / 1024 / 1024).toFixed(2)} MB</td>
<td class="${raf.memoryIncrease < obs.memoryIncrease ? 'winner-cell' : ''}">${(raf.memoryIncrease / 1024 / 1024).toFixed(2)} MB</td>
<td>${memoryImprove > 0 ? '↓' : '↑'} ${Math.abs(memoryImprove)}%</td>
</tr>
</tbody>
</table>
<div class="info-box" style="margin-top: 20px; background: #e8f5e9; border-color: #4CAF50;">
<strong>🎯 结论:</strong>
<ul>
<li><strong>Intersection Observer</strong> 减少了 <strong>${callCountDiff}</strong> 次不必要的滚动事件处理</li>
<li>平均响应时间 ${obs.avgResponseTime < raf.avgResponseTime ? '更快' : '相近'}</li>
<li>总体性能提升约 <strong>${Math.abs(durationImprove)}%</strong></li>
<li><strong>推荐使用 Intersection Observer 方案</strong> ✅</li>
</ul>
</div>
`;
document.getElementById('results-content').innerHTML = resultsHtml;
console.log('📊 对比结果:', {
observer: obs,
raf: raf,
improvement: {
duration: durationImprove + '%',
callCountReduction: callCountDiff,
memory: memoryImprove + '%'
}
});
}
function updateStatus(type, status, text) {
const statusEl = document.getElementById(`status-${type}`);
statusEl.className = `status ${status}`;
statusEl.textContent = text;
}
function updateProgress(type, percent, text) {
document.getElementById(`progress-${type}`).style.width = percent + '%';
document.getElementById(`progress-text-${type}`).textContent = text;
}
function clearTest() {
if (confirm('确定要清空测试吗?')) {
document.getElementById('test-container').innerHTML = '';
testResults = { observer: null, raf: null };
updateStatus('observer', 'ready', '就绪');
updateStatus('raf', 'ready', '就绪');
updateProgress('observer', 0, '等待测试...');
updateProgress('raf', 0, '等待测试...');
document.getElementById('results-content').innerHTML = '<p style="color: #666;">运行测试后将显示对比结果...</p>';
console.log('🗑️ 测试已清空');
}
}
// 页面加载提示
window.addEventListener('load', function() {
console.log('%c═══════════════════════════════════════', 'color: #2196F3; font-weight: bold');
console.log('%c Phase 4.1.3 滚动性能测试', 'color: #2196F3; font-weight: bold');
console.log('%c═══════════════════════════════════════', 'color: #2196F3; font-weight: bold');
console.log('');
console.log('测试流程:');
console.log(' 1. 生成100个表格');
console.log(' 2. 分别运行两种滚动监听方案的测试');
console.log(' 3. 查看对比结果');
});
</script>
</body>
</html>