性能优化:通用快照方案
阿里妹导读
写在前面
性能优化对于提供卓越的用户体验至关重要,钉钉终端团队特别关注用户体验。我们团队采用了一系列创新的性能优化措施,显著提升了首次有意义绘制(FMP)和首次内容绘制(FCP)的性能指标。其中,利用快照方案,结合用户的本地存储能力,我们能够进一步提高页面性能。快照方案是在完成常规手段前端优化(如优化首屏加载体积、实施懒加载、渲染优化和缓存提升等)和资源离线处理之后的又一重要步骤,旨在更迅速地向用户展示页面内容。
钉钉的 PC 工作台通过应用快照技术加速了页面渲染,并从此经验中提炼出了一个通用的快照 SDK,使得其他页面也能轻松集成此技术,从而提高其性能。不仅限于钉钉端内应用本身,同样适用于解决端外等各种场景下的性能提升需求。
接下来,我们将探讨快照技术如何增强页面性能和用户体验,如何在业务中集成快照方案,以及我们的通用快照解决方案的技术细节。
快照方案概述
快照从概念上,这个词从摄影领域借鉴而来,在计算机领域是指在某个特定时间点对系统、数据或配置状态的完整副本,而系统或程序可以利用这一份记录实现快速恢复、启动优化等。
基于快照的性能体验优化手段,主要利用了存储在客户端本地的快照资源,加速页面速度,提升用户体验。
快照方案对前端性能提升的作用:改善首屏显示速度,减少白屏时间。
快照优点:能很好的保存每个用户千人千面的信息,并且有可能和 SSR+流式渲染结合。
使用案例和效果演示
钉钉标准 PC 工作台首页
数据效果
钉钉机器人管理
数据效果
快照 SDK
简介
接入指南
安装
tnpm install @ali/snapshot-dd-webpack-plugin --save-dev# 或ayarn add @ali/snapshot-dd-webpack-plugin --dev
使用
快速体验快照能力版
const SnapshotDDWebpackPlugin = require('@ali/snapshot-dd-webpack-plugin');module.exports = {// ...plugins: [new HtmlWebpackPlugin(),// 新增代码new SnapshotDDWebpackPlugin(),]// ...};
检测快照是否开启成功
修改webpack配置后,tnpm start重启项目。 查看element元素中是否有id为html-snapshot的快照节点,检查其中内容是否符合预期(快照一般会在第二次打开页面才展示快照,第一次打开页面会存储快照)。
精细化调整配置版
const SnapshotDDWebpackPlugin = require('@ali/snapshot-dd-webpack-plugin');module.exports = {// ...plugins: [new HtmlWebpackPlugin(),// 新增代码new SnapshotDDWebpackPlugin({// 可选配置选项config,详细可配置项说明见下文IConfig// 页面根元素id(即react全局挂载容器id),默认取dingapp// rootId: 'mytestid',// 默认为false,使用indexDB存储方式,核心业务可配置true使用localStorage// useLocalStorage: true,// 灰度配置, 仅支持钉钉端内// grayConfig: {// disable: '你的general模块key', // 禁用快照开关key// mobile: 'win_snapshot_enable',// pc: 'pc_snapshot_enable',// mac: '你的general模块key', // 控制mac端能力灰度,仅在mac端生效// win: 'win_snapshot_enable', // 控制win端能力灰度,仅在win端生效// android: 'win_snapshot_enable', // 控制android端能力灰度,仅在android端生效// ios: 'win_snapshot_enable', // 控制ios端能力灰度,仅在ios端生效// },// 自定义处理快照内容,将以该方法返回的内容作为页面快照内容// handleSnapshotHtml: (data) => '<div>test</div>',// 快照内容替换,可配置对快照做微调处理:挖空、替换可能发生改变产生闪烁的元素// snapshotSlotContentMap: {// '.dtm-button-secondary': `<div class='xxx'></div>`,// },// 保存快照成功回调,可进行埋点等操作// takeSnapShotCallback: (data) => console.log('takeSnapShotCallback data:', data),// 快照时机,默认onload之后100ms// takeSnapShotDelay: 1000,// 配置检测到该元素上屏时,执行隐藏快照逻辑,例如'.your-class #yourId'// hideSnapshotSelector: '.dtm-button-secondary',// 移除快照成功回调,可进行埋点等操作// hideSnapShotCallback: (data) => console.log('hideSnapShotCallback data:', data),// 未配置hideSnapshotSelector时,会自动检测 FCP后2s 隐藏快照,配置隐藏的delay时间// 未配置hideSnapshotSelector时,会自动检测 FCP后2s 隐藏快照,配置隐藏的delay时间// hideSnapShotFCPDelay: 2000,// 配置不支持自动FCP的delay隐藏时间,默认3s// hideSnapShotNotSupportFCPDelay: 2000,// debug模式配置,debug模式会有更多log打点// debug: true,})]// ...};
可配置项IConfig
interface IConfig {// 页面根元素id(即react全局挂载容器id),默认取dingapp,若非dingapp,请指定rootId?: string;// 默认为false,使用indexDB存储方式,核心业务可配置true使用localStorageuseLocalStorage?: boolean;// 灰度配置, 仅支持钉钉端内grayConfig?: {disable?: string; // 禁用快照pc?: string; // 控制PC端能力灰度,仅在PC端生效mobile?: string; // 控制移动端能力灰度,仅在移动端生效android?: string; // 控制android端能力灰度,仅在android端生效ios?: string; // 控制ios端能力灰度,仅在ios端生效mac?: string; // 控制mac端能力灰度,仅在mac端生效win?: string; // 控制win端能力灰度,仅在win端生效}// 自定义处理快照内容,将以该方法返回的内容作为页面快照内容handleSnapshotHtml?: string; // (html: string) => string;// 快照内容替换,可配置对快照做微调处理:挖空、替换可能发生改变产生闪烁的元素snapshotSlotContentMap?: {[querySelector: string]: string; // key为任意selector,value为HTML内容的字符串表示};// 保存快照成功回调,可进行埋点等操作takeSnapShotCallback?: string; // (html?: string) => void;// 快照时机,默认onload之后100mstakeSnapShotDelay?: number;// 配置检测到该元素上屏时,执行隐藏快照逻辑,例如'.your-class #yourId'hideSnapshotSelector?: string;// 移除快照成功回调,可进行埋点等操作hideSnapShotCallback?: string; // () => void;// 未配置hideSnapshotSelector时,会自动检测 FCP后2s 隐藏快照,配置隐藏的delay时间hideSnapShotFCPDelay?: number;// 配置不支持自动FCP的delay隐藏时间,默认3shideSnapShotNotSupportFCPDelay?: number;// debug模式配置,debug模式会有更多log打点debug?: boolean;}
灰度开关配置
grayConfig?: {disable?: string; // 禁用快照pc?: string; // 控制PC端能力灰度,仅在PC端生效mobile?: string; // 控制移动端能力灰度,仅在移动端生效android?: string; // 控制android端能力灰度,仅在android端生效ios?: string; // 控制ios端能力灰度,仅在ios端生效mac?: string; // 控制mac端能力灰度,仅在mac端生效win?: string; // 控制win端能力灰度,仅在win端生效}
自定义用法
注意事项
请确保您的webpack配置文件中,HtmlWebpackPlugin已经配置好,否则快照功能无法生效; 请确保您的项目中,页面根元素id若非dingapp,请在配置中指定您的rootId,否则快照功能无法生效; 请确保您的项目中,将css以内联<style>形式打包到html中已经配置好,否则快照功能中样式可能错乱; 默认配置中保存快照、展示快照、移除快照时机均为默认值,若需更加精细化效果呈现,请在配置中调整;
实现方案
快照的作用是什么?
1 工作原理
生成快照:把页面中关键元素的数据缓存到本地存储 展示快照:页面加载过程中,从本地存储中提取出之前保存的快照,并将其作为临时的DOM覆盖在实际页面之上 移除快照:当页面的真实DOM渲染完毕,移除上层的DOM快照,让用户得以看到最新渲染的页面内容
Step1 生成快照
什么时候生成快照?
关键元素
数据
快照形式内容对比
磁盘
存储位置对比
Step2 展示快照
什么时候展示快照?
Step3 移除快照
什么时候隐藏快照?
快照 SDK 效果
<!doctype html><html><head><script>// 增加参数传递部分window.__DD_SNAPSHOT_CONFIG__ = {"rootId": "Root","debug": true,"useLocalStorage": true}</script><!-- <style>你的项目css,需要自行修改webpack配置</style> --><!-- 文件内其他原有内容 --></script></head><body class="bg-common" data-spm="28107366"><!-- 快照挂载的dom节点 --><div id="html-snapshot" style="position: absolute; left: 0; right: 0; top: 0; z-index: 9999999;"></div><script>// 快照展示逻辑!async function() {const n = function() {const n = new URL(window.location.href), {pathname: t, search: e, hash: o} = n;let a = e;e.startsWith("?") && (a = e.substring(1));const r = new URLSearchParams(a), s = [];for (const [n,t] of r)!["dd_mini_app_id", "pc_slide", "dd_darkmode", "dd_progress", "dtaction"].includes(n) && s.push(t);const i = [t.replace(/\.html$/, ""), s.join("_"), o].filter((n=>n)).join("-").replace(/[^a-zA-Z0-9-]/g, "_");return window.__ddSnapshotKey = `ddSnapshotKey_${i}`,window.__ddSnapshotKey}(), t = await async function(n) {let t;return t = window.__DD_SNAPSHOT_CONFIG__ && window.__DD_SNAPSHOT_CONFIG__.useLocalStorage ? localStorage.getItem(n) : await async function(n) {try {const t = await function(n, t, e) {return new Promise(((n,o)=>{const a = indexedDB.open("SnapshotDatabase8");a.onerror = n=>{o("数据库打开失败")},a.onsuccess = a=>{const r = a.target.result.transaction(t, "readonly").objectStore(t).get(e);r.onerror = n=>{o("获取数据失败")},r.onsuccess = t=>{n(t.target.result)}}}))}(0, "snapshot", n);return t} catch (n) {console.error("[snapshot]: Error while loading snapshot:", n)}}(n),t || ""}(n), e = document.getElementById("html-snapshot");if (e && t) {window.__useLocalSnapshotHtmlDD = !0;var o = (new DOMParser).parseFromString(t, "text/html").body;o.firstChild && e.appendChild(o.firstChild),window.performance && window.performance.mark && window.performance.mark("fmp_snapshot"),window.addEventListener("load", (function() {setTimeout((function() {var n = document.getElementById("html-snapshot");n && (n.style.display = "none")}), 1e4)}))}}();</script><!-- 保存快照和移除快照外链脚本 --><script src="https://dev.g.alicdn.com/code/npm/@ali/snapshot-dd-webpack-plugin/1.0.0/debugSnapshot.js?t=1706019210161"></script><div id="Root"></div><!-- html内其他原有内容 --></body></html>
2 适用场景
快照方案适用的场景有哪些?
3 接入时机
快照方案接入的阶段?
4 准确性&稳定性保障
如何保障快照的准确性?
接入快照是否会对业务性能有影响?
根据快照逻辑测试数据,预计耗费时间不超过 20ms(参考文档 SSG 方案 5ms、工作台测试 demo18ms); 需要将 css 打包到 html 内,假如首屏 css 文件体积很大,建议结合离线包方案使用;
异常边界:真实页面加载失败了怎么办?
稳定性保障怎么做?
做好四端设备/不同机型/不同系统的测试:不同机型、系统表现可能会有不同。例如工作台的快照在 mac 端低系统版本会有一个上屏时间检测异常的 bug。 分设备能力灰度,灰度观察时间适当放长,灰度过程观察是否符合预期。
如何预防安全风险?
存储快照时只存储页面的 dom 结构。 展示快照时,避免用户本地的快照内容被篡改,会过滤 script 标签,仅展示 dom 结构部分。
5 优点和限制
快照手段的优点是什么?
快照的限制是什么?
下一步展望
1 效果优化
覆盖率提升
首屏渲染提升
2 方案优化
为了进一步提高页面加载速度和用户体验,我们可以对基于快照的展示方案进行以下优化:
快照作为页面框架: 我们将快照HTML作为基础框架存储在客户端本地。在页面加载时,这一框架被迅速从本地存储中取出并渲染,为用户提供初步的页面结构。 数据注入优化: 利用本地缓存的首页schema数据,我们可以对快照框架进行必要的调整和数据填充。这个过程中,难点在于确保业务改造的成本与复杂度之间的平衡。 渐进式增量渲染: 在JavaScript资源加载并执行后,我们继续“注水”,即将剩余的数据和内容注入到快照框架中,完成页面的最终渲染。