注:某次技术分享的素材
前端范围
chrome(市占率70%)上的web
移动端的webview/webkit
混合开发的React Native、Flutter
桌面端的electron ?(参考:VS Code 启动速度优化 - CovalenceConf 2019: Visual Studio Code – The First Second ?v=r0OeHRUCCb4&ab_channel=ElectronUserland)
业务后端(nodejs)?
前端目标:拓展技术使用的业务边界
原理
简单的全链路流程:客户端-公网端-服务端
不同端有各自的问题和优化方法。比如公网端:
访问源站慢问题,出现CDN分流;
跨运营商网络问题,出现多运营商接入;
公网路由器跳数过多,出现专线;
google统计互联网资源文件大小,提高tcp初始化窗口大小,减少rtt次数;
客户端
web层:
html、css、js、image、font、audio、video
vue、React、RN、flutter、webpack、babel
CSR、SSR、NSR、PWA、wasm
NSR: native side render - 主要是app端使用
应用层-浏览器:chrome
--很多技术出自chromium,可以多多研究
http、ssl\tls
浏览器缓存一般分为两类:强缓存,也称本地缓存;以及弱缓存,也就是协商缓存。
请求一个资源时,会按照优先级(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找缓存
Etag 跟服务器配置有关,每台服务器的 Etag 都是不同的
v8,jit(gc: 新生代+老年代)、aot、wasm、memory-pool、js-CodeCache(memory-buffer)、generated-code(序列号保存到磁盘)、snapshot、js编译引擎sparkplug加速(ignition解释器-sparkplug编译器-TurboFan优化器)
devtools:卡顿:帧率、耗时方法、耗时动作、render次数、冗余render、事件响应耗时、绘图指令延迟
浏览器缓存:
1. 强缓存:
cache-control: req 和response header 都可以带上:
max-age:即最大有效时间,在上面的例子中我们可以看到
must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。
no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。
reponse header会根据情况带上:
last-modified: xxx
etag:
2. 协商缓存(1 > 2):
1. Etag & If-None-Match:
2. Last-Modified & If-Modified-Since
3. 废弃了的expire等用法可以不用怎么管了
// nginx-1.15.8/src/http/ngx_http_core_module.cngx_int_tngx_http_set_etag(ngx_http_request_t *r){...etag->hash = 1;ngx_str_set(&etag->key, "ETag");etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);if (etag->value.data == NULL) {etag->hash = 0;return NGX_ERROR;}//nginx 生成etag值etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"",r->headers_out.last_modified_time,r->headers_out.content_length_n)- etag->value.data;...return NGX_OK;}chrome原理
有种说法:
defer 的脚本被完全缓存时,并没有遵守规范等待解析结束,反⽽阻塞 了解析与渲染?
强制布局
正常布局:串行
强制布局:并行
布局抖动就是多次强制布局导致;
chrome js引擎
chrome js 代码缓存:
真实世界的数据显示,代码缓存命中率(对于可以缓存的脚本)很高(~86%)。虽然这些脚本的缓存命中率很高,但是我们每个脚本缓存的代码量并不是很高。我们的分析表明,增加缓存的代码量可以使 JavaScript 代码的解析和编译减少大约 40% 的时间。
https://v8.js.cn/blog/improved-code-caching/
gc算法:
JavaScript v8 gc:
1. 新生代和老年代;
2. 新生代:标记-复制;空间小,1-8M;Scavenge算法-分A-B区;
3. 老年代:对象大,时间长的放到老年代;多次增量子标记+一起清理;
语言的演化:
JavaScript-最开始作为玩具语言开发出来的,通过js引擎解释执行,速度慢,本身存在很多反常识的坑;
JIT - just in time通过缓存编译结果,直接执行机器码;但是JIT本身也会耗时;
AOT - ahead of time 预编译成机器码,js引擎可以直接运行,缺点是AOT文件过大;
GC - 解决原来需要手动分配和释放内存的方式,期望解决内存安全漏洞,提高开发效率,但GC可能带来stop the world的卡顿,GC本身也演化出各种算法。
TypeScript-采用面向对象的强类型方式通过AST 自动转换为JavaScript ,期望的是解决编码质量和效率问题;
WASM-其它语言源码编译成机器码,浏览器可以直接运行,解决js本身的解释型语言性能跟不上的问题。
OS:
Pid (抢占时间片) : 静态优先级、动态优先级+ 调度算法
CPU(核数+频率+缓存):CPU Cache Line(二维数组遍历)、分支预测、调度算法(CFS)、大小核插拔,cpu提频、taskset、cgroups
MEM:libc(malloc),buddy-system(page alloc),shrink(回收内存),
FS: page-cache(预热,读慢,写快),定时fsync、dirty-page%
DISK: 电梯算法、firmware-cache
flash:SSD 、顺序读写、随机读写、WAL
NET: https,SSL\TLS、HTTP1.1 H2(header压缩,stream并发,push),h3(connect-id); tcp(fast open)、udp, tcp网络栈和NIC(lane)
power:高耗电应用限频
cpu侧:充分利用cpu cache line(64字节大小?)和 分支预测等 提高代码运行效率
TLS优化:
秘钥交换:基于椭圆曲线的 ECDH 密钥协商算法,
数据加密:AES-GCM (并行,cpu支持AES-NI)、CHACHA20算法
升级版本:TLS1.3 将握手时间从 2 个 RTT 降为 1 个 RTT
http 1.1 keep alive、response body 压缩
h2:header 压缩、stream并发(突破6个tcp限制)、server-push(解决rtt)
h3:connection-id:干掉tcp握手
request body: 业务自定义压缩:文本到二进制, json -> protobuff
队头阻塞问题...
这里需要说明的是:
h2的二进制分帧是不是说的对body进行二进制压缩。body是什么就还是什么,如果要对body压缩,response body还是需要开启gzip、br等;request body是可以自己定义的,和后端约定指定格式和压缩算法即可。
网络链路
DNS:跨运营商问题,骨干网
IP
CDN:边缘节点优化,
BGP - AnyCast
服务端
网关
LB
静态资源层:nginx、nodejs
动态资源:SSR、nodejs
微服务:无状态可扩展
数据层:cache、db、mq
docker
k8s
VIP
tcp优化参考:
慢启动
timeout
冲突算法
初始窗口大小
优化
为什么要做性能优化
C端产品:
Pinterest减少⻚⾯加载时⻓40%, 搜索和注册数提⾼了15%
BBC⻚⾯加载时⻓每增加1秒,⽤户流失10%
DoubleClick发现如果移动⽹站加载时⻓超过3秒,53%的⽤户会放弃
B端产品:
节约耗时打开次数用户量 * 薪资成本 = 降本增效
ROI-不要盲目优化:
钱
体验
口碑
练技术
性能优化,有时候不是让好的变得更好,而是让差的变好(用户分级、设备分级、网络分级)
用户对性能延迟的看法
0 至 16 毫秒
用户非常关注轨迹运动,他们不喜欢不流畅的动画。如果每秒渲染 60 个新帧,他们就认为动画很流畅。也就是说,每一帧只有 16 毫秒时间,这包括浏览器将新帧绘制到屏幕所需的时间,因而应用约有 10 毫秒的时间来生成一个帧。
0 到 100 毫秒
在此时间窗口内响应用户操作会让用户觉得结果是即时呈现的。如果时间更长,操作与用户反应之间的联系就会中断。
100 到 1000 毫秒
在此时间窗口内,用户会觉得任务进展基本上是自然连续的。对 Web 上的大多数用户来说,加载页面或更改视图是一项任务。
1000 毫秒或更长
超过 1000 毫秒(1 秒),用户的注意力就会从正在执行的任务上转移。
10000 毫秒或更长
超过 10000 毫秒(10 秒),用户会感到失望,并且可能放弃任务。他们以后可能会回来,也可能不会再回来。
注:用户对性能延迟的感知有所不同,具体取决于网络条件和硬件。
方法
分析完业务全链路流程后:
定指标-量化-监控-分析-优化-防退化
Web指标:
FP
FCP
FMP
DomContentLoaded
OnLoad
Performance api
TTI
Long Task
自定义指标?
Google在RAIL性能评估模型中提出,为了持续吸引⽤户,在 1000 毫秒以内呈现交互内容 同时建议将“⾸屏渲染时间”的终点,视为主⻆元素呈现在屏幕上的时刻
如何定义主⻆元素呈现在屏幕上?
可⻅元素超出屏幕的时刻:自定义指标:统计节点变化
小红书:基于 MutationObserver 观察每⼀次渲染帧 childList,并根据适⽤的算法计算 出 FCP, FMP 等关键渲染帧 timing:
//小红书的指标代码:function(e, t, n) {"use strict";Object.defineProperty(t, "__esModule", {value: !0}),t.observe = function() {if (window.MutationObserver && window.performance && performance.timing) {window.__FP__ = 0,window.__FCP__ = 0,window.__FMP_OBSERVED_POINTS__ = [],window.__FULLY_LOADED__ = 0;var e = void 0,t = 0,n = {},u = void 0,a = void 0; (e = new MutationObserver((function(e) {e.forEach((function(e) {"childList" === e.type ?function e(n) {for (var r = function(r) {var u = n[r];0 === window.__FP__ && u instanceof HTMLBodyElement && (window.__FP__ = Date.now() - performance.timing.navigationStart),0 === window.__FCP__ && (0, i.default)(u) && (window.__FCP__ = Date.now() - performance.timing.navigationStart),u instanceof HTMLElement && (0, o.default)(u) && (_(t, !1),function(e) {function t() {_(e)}u.addEventListener("load", t),u.addEventListener("error", t)} (t++)),u.childNodes && e(u.childNodes)},u = 0; u < n.length; u++) r(u)} (e.addedNodes) : "attributes" === e.type &&function(e) { (0, o.default)(e) && (_(t, !1),function(t) {function n() {_(t)}e.addEventListener("load", n),e.addEventListener("error", n)} (t++))} (e.target)})),function() {if (document.body) {var e = document.body.clientHeight,t = window.innerHeight,n = (0, r.default)(document.body),o = window.__FMP_OBSERVED_POINTS__.length,i = window.__FMP_OBSERVED_POINTS__[o - 1],u = i ? n - i.allElementsNumber: n;window.__FMP_OBSERVED_POINTS__.push({t: Date.now() - performance.timing.navigationStart,layoutSignificance: u / Math.max(1, e / t),allElementsNumber: n})}} ()}))).observe(document, {childList: !0,subtree: !0,attributes: !0,attributeFilter: ["src"]}),function() {var e = XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.send = function() {var n = this;_(t, !1),function(e) {n.addEventListener("readystatechange", (function() {4 === n.readyState && _(e)}))} (t++);for (var o = arguments.length,r = Array(o), i = 0; i < o; i++) r[i] = arguments[i];return e.apply(this, r)}} ()}function _(t) {var o = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1];if (0 === window.__FULLY_LOADED__) if (o) {n[t].end = Date.now();var r = Object.keys(n).some((function(e) {return ! n[e].end}));r || (a = t, u = setTimeout((function() {e.disconnect();var t = n[a].end;window.__FULLY_LOADED__ = t - performance.timing.navigationStart,window.__FMP_OBSERVED_POINTS__ = window.__FMP_OBSERVED_POINTS__.filter((function(e) {return e.t <= window.__FULLY_LOADED__}));var o = new CustomEvent("__fullyloaded__", {detail: {firstPaint: window.__FP__,firstContentfulPaint: window.__FCP__,fullyLoaded: window.__FULLY_LOADED__,observedPoints: window.__FMP_OBSERVED_POINTS__}});window.dispatchEvent(o)}), 2e3))} else {var i = Date.now();n[t] = {start: i};var _ = u && i < n[a].end + 2e3;_ && clearTimeout(u)}}};var o = u(n(2)),r = u(n(3)),i = u(n(4));function u(e) {return e && e.__esModule ? e: {default:e}}},确定标准(tp99?):
前端:秒开
后端:200ms内
分析问题
调试分析:devtools: net、performance、lighthouse;wireshark、burpsuite、Charles、WebPageTest
埋点、监控预警apm、全链路跟踪
录制回放rrweb
devtools:
排队:
资源数量和优先级
chrome 6个tcp并发限制
IO block
SSL:
v1.2->v1.3
秘钥协商:ECDH
数据传输:AES-GCM(AES-NI) 、chacha20(软解) 算法
TTFB:
req size精简:精简header, h2-静态表;
网络链路:cdn、骨干网
服务器处理耗时:各种优化;
下载内容:
响应数据慢:精简、压缩
减少RTT,h2的push
交互优化:
减少JavaScript执行时间;
避免强制同步布局
避免布局抖动
css比js更好
gc导致卡顿
优化
分场景:
加载优化(第一次+第n次)
交互优化
思路:
分类分级
提前-延后-并行、删减
业务和产品层面解决:没有代码就是最快的优化
技术层面解决:浏览器、DNS、IP、CDN、网关、资源服务、API服务
资源分类:不只看页面加载,本质上是看用户体验,减少低端局用户流失比率;
html、js、css;
font(裁剪)、image(@media适配,多分辨率,压缩、webp)、audio/video(压缩、分片、转码、清晰度)
资源分级:
编译期优化:tree shaking:移除dead code,抽取公共代码,按需加载打包,懒加载,冗余依赖版本统一;
提前:cdn(tcp mss initwnd : 10 -> 70;rwnd)、prefetch、preload、cache、BGP-IP(anycast);预加载
事中:减少: js拆包、压缩:coverage-unused、minify、uglify、gzip/br;并行:雪碧图、内联inline、外部引用按需加载、多域名、h2;直接优化: SSL/TLS 算法优化,密码强度、握手次数...
延后:async、defer、setTimeOut、懒加载
关注:CPU: javascript执行速度(LongTask) - runloop耗时优化-火焰图(v8 - js CodeCache、序列化保存磁盘),GPU:UI复杂度-渲染复杂耗时、layout-shift;内存:系统卡顿(OS mem shrink、 lmk)、(浏览器层:net+io+disk)、兼容性导致卡顿
系统级:cpu调度:进程:静态-动态-优先级,cpu绑定、cpu调度算法;net:tcp->quic(h2-h3)、keepalive、header+body 压缩、滑动窗口初始大小配置、socket buffer size: wmem、 rmem、、超时时间、tcp拥塞控制算法:cubic、BBR
API分级:
提前:预请求
事中:接口拆分、接口聚合、后台微服务优化:AKF
延后:
RPC协议: restful:json、grpc:protobuf
设备分级:android设备碎片化,高端机+低端机性能差异巨大- lite轻量版
fake方案:骨架屏、渐进式加载、懒加载、分布渲染、依赖渲染(vue批量渲染导致时序错乱)
服务端优化:
static:nginx disk cache
dynamic:SSR( 对比:SSR : 1700ms , CSR: 2500ms - js-evaluate 耗时严重)
持续保持
事前:版本迭代-退化测试 - 定指标
事后:APM监控,A/B测试
数据量小:全量
数据量大:按用户抽样,按接口抽样、特殊用户用户(boss)-全量
配置开关很重要
Pxx 百分位统计、不是平均值;
CRED:cache、request、error、duration + network(3G、4G、WIFI...)
质量数据、业务数据、反馈数据、舆情数据
埋点-js-sdk -> flink -> Prometheus ;ELK