依旧写前言 前几天看到莫忘的博客有一个站点性能侧边栏,自己也想搞一个。
引用站外链接
试着捣鼓了一下,搞出来了一个差不多的劣质品。这里把教程分享给大家。
最近更新 根因 :site-performance.js 的 updateChart(phase) 原先用
1 var x = SVG_WIDTH - ((CHART_POINTS - 1 - i) + phase) * step;
整根折线随 phase 一起向左平移。于是代表“最新值”的最右端点在每个150ms采样周期内会从x=100退到x≈97.4,新采样一来又瞬间弹回100,导致心脏线最右端抽搐。解决 : 把 updateChart 里的x映射改成最新点死死钉在SVG右边界,旧点向左滚动。
1 2 3 4 var dist = CHART_POINTS - 1 - i;var x = (i === CHART_POINTS - 1 ) ? SVG_WIDTH : SVG_WIDTH - (dist + phase) * step;
最新点始终位于x=100,不会再水平弹回。 其他点继续随phase左滚,保持心脏线的连续滚动感。 最右一段线会随采样轻微伸缩,但配合已有的EMA平滑(新值70%+旧值30%),几乎看不到。 Tips:本次修复借助了AI工具。
效果
指标
含义
数据来源
实时心跳
折线图,反映页面流畅度(60fps≈16.7ms)
每帧 requestAnimationFrame 间隔
页面加载
整页加载耗时
performance.getEntriesByType('navigation')
占用内存
JS 堆内存
performance.memory(仅Chromium内核浏览器)
占用内存只在Chromium内核(Chrome/Edge)显示,Firefox/Safari等浏览器显示“—”属正常现象(为什么?因为我懒)。
前置条件 Hexo博客,SOlitude主题。
教程 js文件 新建source/js/site-performance.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 (function ( ) { 'use strict' ; var CHART_POINTS = 40 ; var SAMPLE_INTERVAL = 150 ; var SVG_WIDTH = 100 ; var SVG_HEIGHT = 30 ; var heartbeatValue = document .getElementById ('perf-heartbeat-value' ); var heartbeatLine = document .getElementById ('perf-heartbeat-line' ); var loadValue = document .getElementById ('perf-load-value' ); var memoryValue = document .getElementById ('perf-memory-value' ); if (!heartbeatValue || !heartbeatLine || !loadValue || !memoryValue) return ; var data = new Array (CHART_POINTS ).fill (SAMPLE_INTERVAL ); var lastSampleTime = performance.now (); var nextSampleTime = lastSampleTime + SAMPLE_INTERVAL ; var lastFrameTime = performance.now (); var frameDeltas = []; var rafId = null ; var warmupId = null ; function formatLoad (ms ) { if (!ms || ms <= 0 ) return '—' ; if (ms < 1000 ) return Math .round (ms) + ' MS' ; return (ms / 1000 ).toFixed (2 ) + ' S' ; } function formatMemory (bytes ) { if (!bytes || bytes <= 0 ) return '—' ; if (bytes < 1024 * 1024 ) return (bytes / 1024 ).toFixed (1 ) + ' KB' ; return (bytes / (1024 * 1024 )).toFixed (1 ) + ' MB' ; } function updateLoadTime ( ) { var nav = null ; try { var entries = performance.getEntriesByType && performance.getEntriesByType ('navigation' ); if (entries && entries.length ) nav = entries[0 ]; } catch (e) {} var ms = 0 ; if (nav && nav.loadEventEnd && nav.loadEventEnd > 0 ) { ms = nav.loadEventEnd - (nav.startTime || 0 ); } else if (window .performance && window .performance .timing ) { var t = window .performance .timing ; if (t.loadEventEnd && t.loadEventEnd > 0 && t.loadEventEnd > t.navigationStart ) { ms = t.loadEventEnd - t.navigationStart ; } } loadValue.textContent = formatLoad (ms); } function updateMemory ( ) { var mem = null ; if (window .performance && window .performance .memory ) { mem = window .performance .memory ; } memoryValue.textContent = mem ? formatMemory (mem.usedJSHeapSize ) : '—' ; } function updateChart (phase ) { var min = Math .min .apply (null , data); var max = Math .max .apply (null , data); var range = Math .max (max - min, SAMPLE_INTERVAL ); var step = SVG_WIDTH / (CHART_POINTS - 1 ); var coords = []; for (var i = 0 ; i < CHART_POINTS ; i++) { var ratio = 1 - (data[i] - min) / range; var y = Math .max (1 , Math .min (SVG_HEIGHT - 1 , ratio * SVG_HEIGHT )); var dist = CHART_POINTS - 1 - i; var x = (i === CHART_POINTS - 1 ) ? SVG_WIDTH : SVG_WIDTH - (dist + phase) * step; coords.push (x.toFixed (2 ) + ',' + y.toFixed (2 )); } heartbeatLine.setAttribute ('points' , coords.join (' ' )); } function tick ( ) { var now = performance.now (); var frameDelta = now - lastFrameTime; lastFrameTime = now; frameDeltas.push (frameDelta); while (now >= nextSampleTime) { var avgDelta = frameDeltas.length ? frameDeltas.reduce (function (a, b ) { return a + b; }, 0 ) / frameDeltas.length : SAMPLE_INTERVAL ; frameDeltas = []; lastSampleTime = nextSampleTime; nextSampleTime += SAMPLE_INTERVAL ; var prev = data[CHART_POINTS - 1 ] || avgDelta; var smoothed = prev * 0.3 + avgDelta * 0.7 ; data.shift (); data.push (Math .max (1 , Math .round (smoothed))); } var phase = Math .min (1 , (now - lastSampleTime) / SAMPLE_INTERVAL ); heartbeatValue.textContent = Math .round (data[CHART_POINTS - 1 ]); updateChart (phase); rafId = requestAnimationFrame (tick); } function startLoop ( ) { if (rafId !== null ) return ; var now = performance.now (); lastFrameTime = now; lastSampleTime = now; nextSampleTime = now + SAMPLE_INTERVAL ; frameDeltas = []; rafId = requestAnimationFrame (tick); } function warmUp ( ) { var samples = []; var start = performance.now (); heartbeatValue.textContent = '…' ; function step ( ) { var now = performance.now (); var delta = now - lastFrameTime; lastFrameTime = now; if (delta > 0 && delta < 100 ) samples.push (delta); if (now - start < 200 && samples.length < 20 ) { warmupId = requestAnimationFrame (step); return ; } var avg = samples.length ? samples.reduce (function (a, b ) { return a + b; }, 0 ) / samples.length : SAMPLE_INTERVAL ; var initial = Math .max (1 , Math .round (avg)); data = new Array (CHART_POINTS ).fill (initial); lastSampleTime = performance.now (); nextSampleTime = lastSampleTime + SAMPLE_INTERVAL ; frameDeltas = []; startLoop (); } warmupId = requestAnimationFrame (step); } function stopLoop ( ) { if (rafId !== null ) { cancelAnimationFrame (rafId); rafId = null ; } if (warmupId !== null ) { cancelAnimationFrame (warmupId); warmupId = null ; } } function init ( ) { updateLoadTime (); updateMemory (); var now = performance.now (); lastSampleTime = now; nextSampleTime = now + SAMPLE_INTERVAL ; lastFrameTime = now; frameDeltas = []; warmUp (); if (document .readyState !== 'complete' ) { window .addEventListener ('load' , updateLoadTime, { once : true }); } } if (document .readyState === 'loading' ) { document .addEventListener ('DOMContentLoaded' , init); } else { init (); } if (!window .__sitePerformancePjaxBound ) { window .__sitePerformancePjaxBound = true ; document .addEventListener ('pjax:complete' , function ( ) { updateLoadTime (); updateMemory (); }); } document .addEventListener ('visibilitychange' , function ( ) { if (document .hidden ) stopLoop (); else startLoop (); }); })();
增加样式 themes/solitude/source/css/_layout/aside.styl在文件末尾追加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 .card-performance position relative padding .8rem 1rem .perf-heartbeat position relative padding .7rem .8rem margin-bottom .75rem background var (--glass-bg) border var (--glass-border) border-radius 12px box-shadow var (--glass-shadow), var (--glass-highlight) -webkit-backdrop-filter blur (12px ) backdrop-filter blur (12px ) overflow hidden .perf-heartbeat-top display flex justify-content space-between align-items baseline margin-bottom .4rem .perf-label font-size 12px color var (--efu-secondtext) .perf-value font-size 18px font-weight 700 color var (--efu-fontcolor) .perf-unit font-size 11px font-weight 500 color var (--efu-secondtext) margin-left 2px .perf-heartbeat-chart width 100% height 34px color var (--efu-main) opacity .85 .perf-grid display grid grid-template-columns 1 fr 1 fr gap 10px .perf-cell padding .6rem .7rem background var (--glass-bg) border var (--glass-border) border-radius 12px box-shadow var (--glass-shadow), var (--glass-highlight) -webkit-backdrop-filter blur (10px ) backdrop-filter blur (10px ) .perf-metric display flex align-items center gap 8px margin-top 4px font-size 16px font-weight 700 color var (--efu-fontcolor) i font-size 18px color var (--efu-secondtext) opacity .7 +maxWidth768 () .perf-grid gap 8px .perf-metric font-size 14px i font-size 16px
如果想要升级不丢,可改成放source/css/performance.css,注意用css语法,记得手动引入。
在source/_data/aside.yml(没有就新建)末尾添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 - name: performance title: 站点性能 class: card-performance icon: fas fa-gauge content_id: card-performance content_html: | <div class="perf-heartbeat"> <div class="perf-heartbeat-top"> <span class="perf-label">实时心跳</span> <span class="perf-value"><span id="perf-heartbeat-value">—</span> <span class="perf-unit">MS</span></span> </div> <svg class="perf-heartbeat-chart" viewBox="0 0 100 30" preserveAspectRatio="none"> <polyline id="perf-heartbeat-line" points="0,15 100,15" fill="none" stroke="currentColor" stroke-width="1.5" vector-effect="non-scaling-stroke"/> </svg> </div> <div class="perf-grid"> <div class="perf-cell"> <div class="perf-label">页面加载</div> <div class="perf-metric"><i class="far fa-clock"></i><span id="perf-load-value">—</span></div> </div> <div class="perf-cell"> <div class="perf-label">占用内存</div> <div class="perf-metric"><i class="fas fa-memory"></i><span id="perf-memory-value">—</span></div> </div> </div>
引入资源 主题配置文件_config.Solitude.yml:
1 2 3 4 5 extends: head: body: - <script defer src="/js/site-performance.js"></script>
让卡片出现在侧边栏
1 2 3 4 5 6 7 aside: home: noSticky: "about,performance" post: noSticky: "about" page: noSticky: "about"
6.hexo三连击 1 2 3 hexo clean hexo generate hexo server
常见问题
占用内存显示“—”:正常,Firefox/Safari不支持performance.memory,仅Chrome / Edge有。
页面加载显示“—”:loadEventEnd偶尔没拿到,刷新或等页面完全加载后会补上。
最后 这个教程是适配我的博客的,之前我大改过我的博客,所以我无法保证在Solitude主题上可以正常使用,请大家谅解(哭哭)。 有任何问题可以找ai询问,如果ai询问无果,可以在评论区里询问(有时间我就看。重点是看心情)。