专栏名称: 阿里开发者
阿里巴巴官方技术号,关于阿里的技术创新均将呈现于此
目录
相关文章推荐
宛央女子  ·  哪吒也不过是在拼爹 ·  23 小时前  
宛央女子  ·  穿真丝内衣的福报来了 ·  23 小时前  
宛央女子  ·  关于大S,大家到底在意难平什么? ·  昨天  
彬彬有理  ·  48岁病逝,谁书写了大S的人生剧本 ·  2 天前  
彬彬有理  ·  大S猝然去世:活到最后是慈悲 ·  3 天前  
51好读  ›  专栏  ›  阿里开发者

性能优化:通用快照方案

阿里开发者  · 公众号  ·  · 2024-02-26 19:36

正文

阿里妹导读


本文我们将探讨快照技术如何增强页面性能和用户体验,如何在业务中集成快照方案,以及我们的通用快照解决方案的技术细节。

写在前面

性能优化对于提供卓越的用户体验至关重要,钉钉终端团队特别关注用户体验。我们团队采用了一系列创新的性能优化措施,显著提升了首次有意义绘制(FMP)和首次内容绘制(FCP)的性能指标。其中,利用快照方案,结合用户的本地存储能力,我们能够进一步提高页面性能。快照方案是在完成常规手段前端优化(如优化首屏加载体积、实施懒加载、渲染优化和缓存提升等)和资源离线处理之后的又一重要步骤,旨在更迅速地向用户展示页面内容。

钉钉的 PC 工作台通过应用快照技术加速了页面渲染,并从此经验中提炼出了一个通用的快照 SDK,使得其他页面也能轻松集成此技术,从而提高其性能。不仅限于钉钉端内应用本身,同样适用于解决端外等各种场景下的性能提升需求。

接下来,我们将探讨快照技术如何增强页面性能和用户体验,如何在业务中集成快照方案,以及我们的通用快照解决方案的技术细节。

快照方案概述

快照 从概念上,这个词从摄影领域借鉴而来,在计算机领域是指在某个特定时间点对系统、数据或配置状态的完整副本,而系统或程序可以利用这一份记录实现快速恢复、启动优化等。

基于快照的性能体验优化手段,主要利用了存储在 客户端 本地的 快照 资源,加速页面速度,提升用户体验。

快照方案对前端性能提升的作用:改善首屏显示速度,减少白屏时间。

快照优点:能很好的保存每个用户 千人千面 的信息,并且有可能和 SSR+流式渲染结合。

使用案例和效果演示


钉钉标准 PC 工作台首页

钉钉标准 PC 工作台通过快照技术提升页面性能:

数据效果

命中快照场景的P80时间 809ms
未命中快照场景 P80 时间 2926ms


钉钉机器人管理

接入快照前后对比视频
BEFORE-无快照
页面有明显加载过程

AFTER-命中快照
页面主要内容快速渲染

数据效果

BEFORE-无快照
理论 FMP:369ms
FCP:229ms


AFTER-命中快照
理论 FMP:52ms
FCP:169ms


快照 SDK


简介

快照技术的核心生效机制包括三个步骤:保存快照、渲染快照和移除快照。我们将这三项功能模块化并提供了配置选项,简化了其他业务的快照能力集成流程。
作用: 通过配置快照的 webpack 插件,使页面自动化具备快照功能;
原理: 该插件会在构建时向项目中修改 html 文件内容,插入快照功能逻辑;
可配置: 支持配置快照内容和关键流程时机、分平台灰度控制能力;
自动数据场景: 接入后会自动在 Feel 平台自动 增加快照场景 ,便于查看快照覆盖率以及进行相关性能感知。


接入指南

anpm 包: https://anpm.alibaba-inc.com/package/@ali/snapshot-dd-webpack-plugin/

安装

tnpm install @ali/snapshot-dd-webpack-plugin --save-dev# 或ayarn add @ali/snapshot-dd-webpack-plugin --dev

使用

在您的webpack配置文件中配置:

快速体验快照能力版

const SnapshotDDWebpackPlugin = require('@ali/snapshot-dd-webpack-plugin');
module.exports = { // ... plugins: [ new HtmlWebpackPlugin(), // 新增代码 new SnapshotDDWebpackPlugin(), ] // ...};
检测快照是否开启成功
  1. 修改webpack配置后, tnpm start 重启项目。
  2. 查看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) => '
test
',

// 快照内容替换,可配置对快照做微调处理:挖空、替换可能发生改变产生闪烁的元素 // snapshotSlotContentMap: { // '.dtm-button-secondary': `
`,
// },
// 保存快照成功回调,可进行埋点等操作 // 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使用localStorage    useLocalStorage?: 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之后100ms takeSnapShotDelay?: number;
// 配置检测到该元素上屏时,执行隐藏快照逻辑,例如'.your-class #yourId' hideSnapshotSelector?: string; // 移除快照成功回调,可进行埋点等操作 hideSnapShotCallback?: string; // () => void;
// 未配置hideSnapshotSelector时,会自动检测 FCP后2s 隐藏快照,配置隐藏的delay时间 hideSnapShotFCPDelay?: number; // 配置不支持自动FCP的delay隐藏时间,默认3s hideSnapShotNotSupportFCPDelay?: number;
// debug模式配置,debug模式会有更多log打点 debug?: boolean;}
灰度开关配置
注意:目前依赖钉钉 JSAPI, 仅支持钉钉端内
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端生效}
请在钉钉gray平台创建general模块的key,可选以下纬度按需配置灰度key 。
1、【可选】禁用快照开关,不区分设备,优先级最高,默认值为false,灰度到的用户值为true,则无法使用快照 ;
2、【可选】按照平台类型建立的灰度key,用于灰度,可按照PC、移动端、Mac、Win、Android、iOS纬度进行灰度;

自定义用法

SDK 支持透出takeSnapshot 、removeSnapshot 方法,业务在项目中自行调用。

注意事项

  1. 请确保您的webpack配置文件中,HtmlWebpackPlugin已经配置好,否则快照功能无法生效;
  2. 请确保您的项目中,页面根元素id若非dingapp,请在配置中指定您的rootId,否则快照功能无法生效;
  3. 请确保您的项目中,将css以内联 形式打包到html中已经配置好,否则快照功能中样式可能错乱;
  4. 默认配置中保存快照、展示快照、移除快照时机均为默认值,若需更加精细化效果呈现,请在配置中调整;

实现方案

我们先一起回顾下

快照的作用是什么?

快照机制极大缩短了用户等待JavaScript资源加载并解析后页面完成渲染的时间。通过采取这样一种在 HTML 中尽早渲染快照的策略,我们能够优化页面的加载过程,提前页面的内容渲染,减少用户等待的白屏时间。


1 工作原理

  • 生成快照:把页面中关键元素的数据缓存到本地存储
  • 展示快照:页面加载过程中,从本地存储中提取出之前保存的快照,并将其作为临时的DOM覆盖在实际页面之上
  • 移除快照:当页面的真实DOM渲染完毕,移除上层的DOM快照,让用户得以看到最新渲染的页面内容

Step1 生成快照

把页面中关键元素的数据缓存到本地存储。

什么时候生成快照?

一般情况下,在页面渲染完成之后,即页面 onload 。

关键元素

快照的内容包括页面哪些部分?
对于首屏内容多变的场景,可以只对页面中每次基本不变的部分进行快照,使首屏部分内容实现秒出的同时避免快照闪烁。对于页面中一些不适合快照的部分,可以选择挖空或者替换为骨架屏的方式。

数据

快照内容是什么形式?
快照内容可以是两种:HTML 或图片,因 HTML 形式具备便于数据处理,并且可拓展性强的优点,采用 HTML 形式
快照形式内容对比

本地存储
生成的快照存放在哪里?
考虑到端内各业务在同一域名下共用 localStorage 内存,在快照 SDK 中默认将放在 indexDB 中,支持通过配置项使用 localStorage。(配置项中通过传入 useLocalStorage 参数控制)
考虑到 indexDB 空间够用,故没有使用磁盘。
✅ localStorage
✅ indexDB
  • 磁盘
存储位置对比
存储方式
存储速度
存储空间
适用场景
localStorage
较快
约 5MB
存储简单数据或需要快速读写
indexDB
较慢
250MB 以上
需要存储大量结构化数据或需要进行复杂数据操作
磁盘
较慢
大(取决于用户磁盘)
端内支持调用 jsapi 场景
ServiceWorker


兼容性问题?

Step2 展示快照

什么时候展示快照?

在 HTML 中尽早展示快照逻辑。SDK 中会将展示逻辑插入到内 dom 节点之后,建议接入快照后,把 html 内容中快照逻辑前的逻辑(加载脚本等)后置。

Step3 移除快照

什么时候隐藏快照?

在页面的真实 DOM 渲染完成时,实现快照和真实页面的无缝衔接

快照 SDK 效果

自动完成快照三个功能的注入
<html>    <head>        <script>            // 增加参数传递部分            window.__DD_SNAPSHOT_CONFIG__ = {                "rootId": "Root",                "debug": true,                "useLocalStorage": true            }script>                        script>    head>    <body class="bg-common" data-spm="28107366">                <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






请到「今天看啥」查看全文