性能优化:通用快照方案

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

写在前面

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


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


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


快照方案概述

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


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

image.png

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


image.png


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


使用案例和效果演示


钉钉标准 PC 工作台首页

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


image.png


数据效果

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


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


image.png

钉钉机器人管理

接入快照前后对比视频

image.png

数据效果

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

image.png

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

image.png


快照 SDK

简介

快照技术的核心生效机制包括三个步骤:保存快照、渲染快照和移除快照。我们将这三项功能模块化并提供了配置选项,简化了其他业务的快照能力集成流程。


作用:通过配置快照的 webpack 插件,使页面自动化具备快照功能;


原理:该插件会在构建时向项目中修改 html 文件内容,插入快照功能逻辑;


可配置:支持配置快照内容和关键流程时机、分平台灰度控制能力;


自动数据场景:接入后会自动在 Feel 平台自动增加快照场景,便于查看快照覆盖率以及进行相关性能感知。


image.png

接入指南

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) => '<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使用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以内联<style>形式打包到html中已经配置好,否则快照功能中样式可能错乱;
  4. 默认配置中保存快照、展示快照、移除快照时机均为默认值,若需更加精细化效果呈现,请在配置中调整;


实现方案

我们先一起回顾下

快照的作用是什么?

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


image.png


1 工作原理

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

Step1 生成快照

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

什么时候生成快照?

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

关键元素

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

数据

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

快照形式内容对比

image.png


本地存储

生成的快照存放在哪里?考虑到端内各业务在同一域名下共用 localStorage 内存,在快照 SDK 中默认将放在 indexDB 中,支持通过配置项使用 localStorage。(配置项中通过传入 useLocalStorage 参数控制)考虑到 indexDB 空间够用,故没有使用磁盘。


✅ localStorage

✅ indexDB

  • 磁盘
存储位置对比
存储方式 存储速度 存储空间 适用场景
localStorage 较快 约 5MB 存储简单数据或需要快速读写
indexDB 较慢 250MB 以上 需要存储大量结构化数据或需要进行复杂数据操作
磁盘 较慢 大(取决于用户磁盘) 端内支持调用 jsapi 场景
ServiceWorker

兼容性问题?


Step2 展示快照

什么时候展示快照?

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

Step3 移除快照

什么时候隐藏快照?

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

快照 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  适用场景

快照方案适用的场景有哪些?

image.png

3  接入时机

快照方案接入的阶段?

适合在性能优化的后期阶段,此时已经完成了大量基本性能提升措施:诸如减小资源包体积、优化渲染流程、完善数据接口效率、加强缓存机制以及接入离线包技术等策略之后,适合考虑引入快照技术。


4  准确性&稳定性保障

如何保障快照的准确性?

通过以下措施保障快照内容的可控性稳定性1、选取页面中多次刷新页面展示不变的部分作为快照内容;2、对快照做微调处理:挖空、替换可能发生改变、产生闪烁的元素/模块,例如 PC 工作台将不可预测的插件内容进行了骨架模版的替换;

接入快照是否会对业务性能有影响?

  • 根据快照逻辑测试数据,预计耗费时间不超过 20ms(参考文档 SSG 方案 5ms、工作台测试 demo18ms);
  • 需要将 css 打包到 html 内,假如首屏 css 文件体积很大,建议结合离线包方案使用;

异常边界:真实页面加载失败了怎么办?

对展示快照的时间设置一个展示的兜底时间,如果展示时间达到上限时,首屏仍然没有渲染成功,那么快照将直接隐藏。然后1、展示页面的真实加载失败的情况。2、进一步优化:展示快照部分的 HTML 结构

稳定性保障怎么做?

  • 做好四端设备/不同机型/不同系统的测试:不同机型、系统表现可能会有不同。例如工作台的快照在 mac 端低系统版本会有一个上屏时间检测异常的 bug。
  • 分设备能力灰度,灰度观察时间适当放长,灰度过程观察是否符合预期。

如何预防安全风险?

  • 存储快照时只存储页面的 dom 结构。
  • 展示快照时,避免用户本地的快照内容被篡改,会过滤 script 标签,仅展示 dom 结构部分。

5  优点和限制

快照手段的优点是什么?

1、 利用用户的本地缓存,无额外服务器成本。2、 快照能很好的保存用户千人千面的信息,相比统一骨架屏,具备适配千人千面的能力。3、 快照手段可以是对 SSR、CSR 或离线包等端侧性能优化手段的补充。

快照的限制是什么?

快照缓存的生效前提是二次访问,重复访问率较高的页面快照覆盖率高,效果会明显一些。


下一步展望


1  效果优化

覆盖率提升

目前PC工作台的快照命中率在82%左右,ACTION 是如何提升覆盖率。

首屏渲染提升

快照内容优化:保存快照时计算组件高度,仅保存&展示首屏内容,加速快照真实上屏时间。


2  方案优化

为了进一步提高页面加载速度和用户体验,我们可以对基于快照的展示方案进行以下优化:

  • 快照作为页面框架: 我们将快照HTML作为基础框架存储在客户端本地。在页面加载时,这一框架被迅速从本地存储中取出并渲染,为用户提供初步的页面结构。
  • 数据注入优化: 利用本地缓存的首页schema数据,我们可以对快照框架进行必要的调整和数据填充。这个过程中,难点在于确保业务改造的成本与复杂度之间的平衡。
  • 渐进式增量渲染: 在JavaScript资源加载并执行后,我们继续“注水”,即将剩余的数据和内容注入到快照框架中,完成页面的最终渲染。


与原有方案相比,此优化策略的关键在于,从本地存储中提取的快照不只是作为临时覆盖层来加速页面展示,而是作为实际页面的起始点,随后通过增量更新实现完整同构渲染。

此方法与服务器端生成静态页面(SSG)的差异在于,HTML模板的生成转移到了客户端,而不是在服务器端处理。这种做法有效地利用了客户端的本地缓存能力,不仅减少了服务器的负载,还可能降低数据传输量,从而提高了整体的页面加载性能。

image.png

方案对比

image.png

来源|阿里云开发者公众号

作者|星迎




相关文章
|
存储 NoSQL 算法
8个 数据库性能优化方案,你知道几个?(建议收藏) 上
8个 数据库性能优化方案,你知道几个?(建议收藏) 上
893 0
8个 数据库性能优化方案,你知道几个?(建议收藏) 上
|
2月前
|
存储 缓存 安全
【C/C++ 项目优化实战】 分享几种基础且高效的策略优化和提升代码性能
【C/C++ 项目优化实战】 分享几种基础且高效的策略优化和提升代码性能
65 0
|
3月前
|
消息中间件 缓存 监控
项目接口性能优化方案
项目接口性能优化方案
37 1
|
9月前
|
弹性计算 监控 云计算
高效性能与稳定监控:ECS性能优化与监控实践
本文深入研究了云服务器ECS的性能优化与监控策略,重点关注了优化实例性能的方法与技巧、调整操作系统参数,以及监控指标的选择、监控工具的应用和建立告警机制。通过实际代码示例,读者能够全面了解如何提升实例性能、实现稳定监控,以确保业务的高效性与可靠性。
215 0
|
9月前
|
编译器 C++ Anolis
性能优化特性之:PGO
本文介绍了倚天实例上的编译优化特性:PGO,并从优化原理、使用方法进行了详细阐述。
|
9月前
|
存储 编译器 C语言
性能优化特性之:LTO
本文介绍了倚天实例上的编译优化特性:LTO,并从优化原理、使用方法进行了详细阐述。
|
10月前
|
存储 缓存 NoSQL
性能优化方案及思考
周末闲暇在家,朋友让我帮忙优化一个接口,这个接口之前每次加载都需要40s左右,经过优化将性能提了10倍左右;又加了缓存直接接口响应目前为300ms左右,于是将自己的优化思路整理总结一下
|
10月前
|
存储 缓存 JSON
聊聊方案中心性能优化中做的缓存设计
总结国际站方案中心物流运费计算性能优化过程中面临问题、问题分析、解决思路以及整体解决方案
聊聊方案中心性能优化中做的缓存设计
|
12月前
|
SQL 缓存 NoSQL
记一次接口性能优化实践总结:优化接口性能的八个建议
最近对外接口偶现504超时问题,原因是代码执行时间过长,超过nginx配置的15秒,然后真枪实弹搞了一次接口性能优化。在这里结合优化过程,总结了接口优化的八个要点,希望对大家有帮助呀~
|
存储 缓存 NoSQL
高性能的本地缓存方案选型,看这篇就够了!
高性能的本地缓存方案选型,看这篇就够了!
22734 0