前端性能优化¶
前言¶
性能优化意义何在¶
- 高性能 → 用户参与度 & 用户留存 → 高转换率 & SEO 排名 → 业务收益
- 业界经验:WPO Stats 性能优化案例库
优化做些什么?¶
- First Request
- Fetch Resources
- Decompress, Parse/Compile, Render
优化的标准¶
- Loading: 1000ms
- Finger Down: max 100ms
- Animation: max 6ms
- Finger Up: 50ms
优化方法大纲¶
- 性能优化指标与测量工具
- 渲染优化
- 代码优化
- 资源优化
- 构建优化
- 传输加载优化
- 更多流行优化技术
性能指标与测量工具¶
性能优化指标:加载¶
-
Waterfall 资源加载
-
基于 HAR 存储与重建性能信息
- 保存为 HAR (HTTP Archive format) 文件
- 用性能分析工具查看
-
Lighthouse
-
重要测量指标
- Speed Index
- TTFB
- 页面加载时间
- 首次渲染
性能优化指标:响应¶
-
渲染帧速监控
-
交互动作的反馈时间
- 异步请求的完成时间:要求 <= 1s 完成 or loading 动画
RAIL 测量模型¶
-
介绍
- RAIL是一个以用户为中心的性能模型,它把用户的体验拆分成几个关键点(例如 tap、scroll、load),并且帮你定义好了每一个的性能指标。
-
Response 响应
- 图示
- 定义:用户交互响应是否及时, 处理事件应该在 50ms 以内完成
- 目标:用户的输入到响应的时间不超过100ms,给用户的感受是瞬间就完成了。
-
优化方案
- 事件处理函数在50ms内完成,考虑到idle task的情况,事件会排队,等待时间大概在50ms。适用于click,toggle,starting animations等,不适用于drag和scroll。
- 复杂的js计算尽可能放在后台,如web worker,避免对用户输入造成阻塞
- 超过50ms的响应,一定要提供反馈,比如倒计时,进度百分比等。
-
Animation 动画
- 定义:动画是否足够流畅,每 10ms 产生一帧。
-
目标
- 产生每一帧的时间不要超过10ms,为了保证浏览器60帧,每一帧的时间在16ms左右,但浏览器需要用6ms来渲染每一帧。
- 旨在视觉上的平滑。用户对帧率变化感知很敏感。
-
优化方案
- 在一些高压点上,比如动画,不要去挑战cpu,尽可能地少做事,如:取offset,设置style等操作。尽可能地保证60帧的体验。
-
Idle 空闲
- 定义:最大化空闲时间
- 目标:让浏览器有足够的空闲时间,处理交互;以增大50ms内响应用户输入的几率。
-
优化方案
- 空闲时间来完成一些延后的工作,如先加载页面可见的部分,然后利用空闲时间加载剩余部分,此处可以使用 requestIdleCallback API
- 在空闲时间内执行的任务尽量控制在50ms以内,如果更长的话,会影响input handle的pending时间
- 如果用户在空闲时间任务进行时进行交互,必须以此为最高优先级,并暂停空闲时间的任务
-
Load 加载
- 定义:传输内容到页面可交互的时间不超过 5 秒
-
目标
- 优化加载速度,可以根据设备、网络等条件。目前,比较好的一个方式是,让你的页面在一个中配的3G网络手机上打开时间不超过5秒
- 对于第二次打开,尽量不超过2秒
-
优化方案
- 可以采用
lazy load
,code-splitting
等 其他优化手段,让第一次加载的资源更少
- 可以采用
性能测量工具¶
-
Chrome DevTools 开发调试工具
- Audit(Lighthouse)
- Throttling 调整网络吞吐
- Performance 网络性能分析
- Network 网络加载分析
-
Lighthouse 网站整理质量评估
- 安装
# 安装 $ npm install -g lighthouse # 检测 $ lighthouse http://www.baidu.com
-
WebPageTest 多测试地点,全面性能报告
- waterfall chart 请求瀑布图
- first view 首次访问
- repeat view 二次访问
- 安装
# 运行 server 实例 $ docker run -d -p 4000:80 webpagetest/server # 运行 agent 实例 $ docker run -d -p 4001:80 \ --network="host" \ -e "SERVER_URL=http://localhost:4000/work/" \ -e "LOCATION=Test" \ webpagetest/agent
性能测量 APIs¶
- 关键时间节点 (Navigation Timing, Resource Timing)
- 网络状态 (Network APIs)
- 客户端服务端协商 (HTTP Client Hints) & 网页显示状态 (UI APIs)
- Timing 指标
DNS 查询耗时: domainLookupEnd - domainLookupStart
TCP 链接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectStart
网络请求请求耗时(TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte 时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadedEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
HTTP 头部大小: transferSize - encodedBodySize
重定向次数: performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart
-
示例
- Case 1. 可交互时间(TTI)
window.addEventListener('load', event => { const timing = performance.getEntriesByType('navigation')[0]; // Time To Interactive 可交互时间 let tti = timing.domInteractive - timing.fetchStart; console.log('TTI:', tti); });
- Case 2. 页面不可见
/* * 页面不可见时可以做节流等优化 */ let vEvent = 'visibilitychange'; if (document.webkitHidden !== undefined) { vEvent = 'webkitvisibilitychange'; } window.addEventListener(vEvent, () => { if (document.hidden || document.webkitHidden) { console.log('页面不可见!'); } else { console.log('页面可见!'); } }, false);
- Case 3. 网络状态监控
/* * 监控网络状态 */ const con = navigator.connection || navigator.webkitConnection; let type = con.effectiveType; con.addEventListener('change', () => { console.log('connection type changed from ' + type + ' to ' + con.effectiveType); type = con.effectiveType; });
渲染优化¶
现代浏览器渲染原理¶
-
关键渲染路径
- 关键渲染路径,即 Critical Rendering Path;
- 浏览器渲染流程:
Javascript -> Style -> Layout -> Paint -> Composite
。 - 图示
-
核心阶段
-
Javascript 阶段: Running JavaScript while Build DOM & Build CSSOM。
- 创建 DOM 树。DOM 树是 HTML 页面完全解析后的一种树状表示方式。
- 创建 CSSOM 树。CSSOM 树是对附在DOM结构上的样式的一种表示方式。每个节点都带上样式 ,包括明确定义的和隐式继承的。
- 执行JavaScript。JavaScript是一种 解析阻塞资源(parser blocking resource),它能阻塞HTML页面的解析。
-
Style 阶段:Creating the Render Tree。
- 渲染树是 DOM 和 CSSOM 的结合,是最终能渲染到页面的元素的树形结构表示。
- 只包含能在页面中最终呈现的元素,而不包含那些用CSS样式隐藏的元素,比如带有
display: none;
属性的元素。
-
Layout 阶段:Generating the Layout。
- 布局决定了视口的大小,为 CSS 样式提供了依据,比如百分比的换算或者视口的总像素值。
-
Paint 阶段:绘制。
- 页面上可见的内容就会转化为屏幕上的像素点。
-
Composite 阶段:合成。
- 浏览器为了保证效率,将元素画在不同图层,最后对这些图层合成在一起。
-
-
要点
- CSS是一种 渲染阻塞资源(render blocking resource),它需要 完全被解析完毕之后才能进入生成渲染树的环节。
- 因为 JavaScript脚本的执行必须等到 CSSOM 生成之后,所以说CSS也会 阻塞脚本(script blocking)。
- JavaScript是一种 解析阻塞资源(parser blocking resource)。浏览器遇到 script 标记,DOM 停止构建,若 CSSOM 正在下载和构建,则等其下载和构建完毕,执行脚本,执行完之后 DOM 接着构建。
- 但是script标签有 async属性 的时候,就可以指示浏览器在等待脚本可用期间不阻止 DOM 构建。
可优化的渲染环节及方法¶
-
布局和绘制
- 关键渲染路径上代价最高的两个步骤:Layout 和 Paint。
-
回流诱因
- 添加/删除元素
- 操作 styles
- display: none
- offsetLeft、scrollTop、 clientWidth
- 移动元素位置
- 修改浏览器大小,字体大小
-
布局抖动 Layout Thrashing
- 定义:避免频繁修改 DOM 元素,而导致 回流 发生。
- 避免回流:比如 vDOM 机制
-
读写分离
- 批量读取部署信息,批量进行修改
- FastDOM
-
案例
/* * 布局抖动 * 如下,循环读取 offsetTop,并修改元素样式会导致 布局抖动 */ let images = []; // 一系列图片元素 function update(timestamp) { for (let i = 0; i < images.length; i++) { images[i].style.width = (Math.sin(images[i].offsetTop + timestamp / 1000) + 1) * 500 + 'px'; } window.requestAnimationFrame(update); }
- 优化
/* * 解决布局抖动 * FastDOM 进行读写分离 */ let images = []; // 一系列图片元素 function update(timestamp) { for (let i = 0; i < images.length; i++) { fastdom.measure(() => { // 读:读取 top 值 let top = images[i].offsetTop; fastdom.mutate(() => { // 写: 设置新的 width images[i].style.width = (Math.sin(top + timestamp / 1000) + 1) * 500 + 'px'; }); }); } window.requestAnimationFrame(update); }
-
图层与复合线程
-
复合线程(compositor thread)
- 将页面拆分图层(layers) 进行绘制再进行复合
-
4 things a browser can animate cheaply: 只触发 composite,而不引发 layout 和 paint。
- Position: transform: translate(npx, npx);
- Scale: transform: scale(n);
- Rotation: transform: rotate(ndeg);
- Opacity: opacity: 0...1;
-
-
减少重绘 (Repaint)
- 利用 will-change 创建新的图层
- 利用 DevTools 识别 paint 的瓶颈,判断是否有必要创建图层
- 尽量使用 transform 和 opacity 来处理动画效果
-
防抖(Debonce)
- 高频事件处理函数: 可能在一帧里多次触发,会导致 CPU 负担变重。
-
debounce 实现
- setTimeout 实现,可通过 delay 指定触发频率
- requestAnimationFrame 实现,按照每帧一次的频率触发
/* * requestAnimationFrame 实现防抖 */ function debounce(fn) { let ticking = false; return function () { if (ticking) { return ; } ticking = true; window.requestAnimationFrame(() => { fn(); ticking = false; }); } }
-
React 时间调度
- 一帧的渲染周期
- requestIdleCallback 定义:在一帧剩余时间执行任务; chrome 官方提供的一个标准。
-
- 双向环形链表,根据优先级,调度任务
- 利用 requestAnimationFrame 实现 requestHostCallback,执行 React 的调度任务
代码优化¶
JavaScript¶
-
1)代码开销
- 加载(load) -> 解析&编译(parse/compile) -> 执行(execute)
- 开销对比 170KB:Javascript VS Image
- 页面所有代码中,JavaScript 开销最为昂贵,最可能成为性能短板。
-
2)加载优化
- Code splitting 代码拆分,按需加载。
- Tree shaking 代码减重。
-
3)解析/执行优化
-
减少主线程工作量
- 避免长任务(Long Task)
- 避免超过 1kB 的 行内代码:因为阻塞 DOM 解析,导致加载渲染受阻变慢;浏览器没办法对行内代码进行优化
- 使用 rAF 和 rIC 进行时间调度
-
渐进式启动(Progressive Bootstrapping)
- 可见不可见交互 vs 最小可交互资源集
-
HTML¶
-
减少 iframes 使用
- 阻碍父文档加载过程
- 可使用延迟加载
-
压缩空白符,删除注释
- 避免深层次嵌套
- 避免 table 布局
- CSS&Javascript 尽量外链
- 删除默认属性
- 借助工具: html-minifier
CSS¶
- 用 performance 查看 Recaculate Style 的开销
-
降低 CSS 对渲染的阻塞
- 尽早加载,减小体积
-
利用 GPU 进行完成动画
- will-change 等
-
使用 contain 属性
- contain 内部子元素布局,与外部无关,不会导致其他元素重绘
- render tree 阶段,样式计算时,不继承父样式
-
使用 font-display 属性
- 解决 FOUC 文字闪动问题,让文字尽早展示
附:Javascript 编译原理¶
-
V8 引擎
- 图示
- 正常编译:源码 -(parser)-> 抽象语法树 -(interpreter)-> 字节码 Bytecode
- 优化:字节码 Bytecode -(compiler)-> 机器码
- 反优化:机器码 -> 字节码 Bytecode
- 编译过程中 会进行优化;运行时 可能发生反优化
- 目的:了解 V8 引擎基本原理,写出一些 V8 可以帮忙优化的代码,避免在运行时可能发生的反优化。
- Case 1. 反优化
// de-opt.js const { performance, PerformanceObserver } = require('perf_hooks'); const add = (a, b) => a + b; const num1 = 1; const num2 = 2; performance.mark('start'); for (let i = 0; i < 10000000; i++) { add(num1, num2); } // add(num1, '字符串'); // 比较有无这行代码的执行时间 for (let i = 0; i < 10000000; i++) { add(num1, num2); } performance.mark('end'); const observer = new PerformanceObserver(list => { console.log(list.getEntries()[0]); }); observer.observe({ entryTypes: ['measure'] }); performance.measure('测量1', 'start', 'end');
node --trace-opt --trace-deopt de-opt.js
-
V8 优化机制
-
脚本流
- 下载 -> 解析/编译 -> 运行
- 下载脚本达到足够大,V8 单独开启一个线程,给这一段代码进行解析;最后所有脚本下载解析完成,合并解析结果。
-
字节码缓存
- 频繁使用的相同逻辑的字节码缓存起来,不再重复解析,直接复用。
-
懒解析
- 对于声明的函数,不马上解析(不用生成语法树,不用分配空间),真正调用时再解析。
-
-
函数优化
- 懒解析 lazy parsing vs 饥饿解析 eager parsing
- 利用 Optimize.js 优化初次加载时间
- Case 1. 解析
/* 1. 懒解析 */ export default () => { const add = (a, b) => a + b; // 默认懒解析 add(1, 2); // 调用时才解析 };
- Case 2. 饥饿解析
/* 2. 饥饿解析 */ export default () => { const add = ((a, b) => a + b); // 强制饥饿解析 add(1, 2); // 直接调用,不必再解析 };
-
可在 weppack 打包时将以上模块单独打包;用 performance 查看比较该模块的 Evaluate Script 阶段的总时长。
-
对象结构及优化
-
对象结构
- 隐藏类(Hidden Class)
- 可索引属性(Elements)
-
命名属性(Properties)
- 对象内属性(in-object)
- 快属性(fast)
- 慢属性(slow)
-
对象优化
- 以相同的顺序初始化成员对象,避免 隐藏类(Hidden Class) 的调整
- 实例化后避免添加属性
- 尽量使用 Array 代替 array-like 对象
-
避免读取超过数组的长度
- 会沿着原型链查找越界属性,造成额外开销
-
避免元素类型的转换
let arr = [1, 2, 3]; arr.push('x');
- 编译器判断元素类型,进行优化;
- 若发现类型转换,之前的优化失效。
-
Case 1
/* * case 1 * 以相同的顺序初始化成员对象,避免隐藏类的调整 */ class Foo { // Hidden Class 0 constructor(a, b) { this.a = a; // Hidden Class 1 this.b = b; // Hidden Class 2 } } // 可以复用 Hidden Class const foo1 = new Foo(1, 2); const foo2 = new Foo(3, 4); // 否则,需要重新创建 Hidden Class const bar1 = { x: 'a' }; // HC 0 bar1.y = 2; // HC 1 const bar2 = { y: 3 }; // HC 2 bar1.x = 'b'; // HC 3
- Case 2
/* * case 2 * 实力化后避免添加属性 */ // x: in-object 属性,存储在堆 const bar1 = { x: 'a' }; // y: Normal/Fast 属性 // 存储在 properties store 里,需要通过描述数组间接查找 bar1.y = 2;
- Case 3
/* * case 3 * 尽量使用 Array 代理 array-like 对象 */ Array.prototype.forEach.call(arrLikeObj, (val, index) => { console.log(val, index); }); // 以上操作不如在真实数组操作效率高 const arr = Array.prototype.slice.call(arrLikeObj, 0); arr.forEach((val, index) => { console.log(val, index); });
- Case 4
/* * case 4 * 避免读取超过数组的长度 */ const arr = []; for (let i = 0; i < arr.length + 1; i++) { console.log(arr[i]); // 会沿着原型链查找,造成额外开销 }
-
资源优化¶
Guideline¶
- 压缩 & 合并
- 图片格式
- 图片加载
- 字体优化
HTML 压缩¶
- 使用在线工具: terser
- 使用 html-minifier 等 npm 工具
CSS 压缩¶
- 使用在线工具
- 使用 clean-css 等 npm 工具
JS 压缩与混淆¶
- webpack
构建优化¶
Guideline¶
- webpack 的优化配置
- 代码拆分
- 代码压缩
- 持久化缓存
- 监测与分析
- 按需加载
Webpack Mode¶
-
Convention Over Configuration: meaning, definition, explanation。
- 图示
- ModuleConcatenationPlugin: 作用域提升
- TerserPlugin: 代码体积减少,Tree Shaking
Tree-shaking¶
- 上下文中未用到的代码(dead code)
- 只识别基于 ES6 import & export
-
注意 Babel 默认配置的影响
@babel/preset-env
配置modules: fasle
,保留 ES6 模块化语法
-
package.json 中配置 sideEffects
- terser 识别副作用特别的耗时且不准确
- 告诉 terser 哪些文件有副作用或者所有文件都没有副作用,提升打包速度
- 示例
// package.json { "sideEffects": [ "dist/*", "es/**/style/*", "lib/**/style/*", "*.css" ] }
作用域提升¶
-
代码体积较小
- 提升执行效率
- 同上,注意 Babel 的 modules 的配置
Bable 7 配置¶
- 在需要的地方 pollyfill
-
辅助函数 的按需引入
- 比如,每次声明类时都会生成一个叫 _classCallCheck 的辅助函数;
- 通过配置 babel/plugin-transform-runtime,复用辅助函数
-
根据目标浏览器按需转换代码
- 示例
// 源码
class A {}
// 经过babel 转换
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var A = function A() {
_classCallCheck(this, A);
};
_classCallCheck 就是一个辅助函数。
Webpack 依赖优化¶
-
noParse
- 提升构建速度
- 直接通知 webpack 忽略较大的库,比如 lodash
- 被忽略的库不能有 import, require, defined 的引入方式
-
DllPlguin
- 避免打包时对不变的库重复构建, 提升构建速度
Webpack 代码拆分¶
- Code Splitting, 把单个 bundle 文件拆分成若干小 bundles/chunks
- 缩短首屏加载时间
- 配置 splitChunks 提取公有代码,拆分业务代码与第三方库
- 动态加载
- 示例
optimization: {
splitChunks: {
cacheGroups: {
// 拆分第三方库
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
minSize: 0, // 将所有模块独立出来
minChunks: 1, // 最少拆成 1 段
priority: 10,
chunks: 'initial'
},
// 拆分业务代码的公共模块
common: {
name: 'common',
test: /[\\/]src[\\/]/,
minSize: 0,
minChunks: 2,
chunks: 'all'
}
}
}
}
Webpack 资源压缩(Minification)¶
- Terser 压缩 JS
-
压缩 CSS
- 1) mini-css-extract-plugin 抽取出 css 文件
- 2) optimize-css-assets-webpack-plugin 对抽取的 css 进行压缩
-
html-webpack-plugin/minify 压缩 HTML
Webpack 资源持久化缓存¶
- 每个打包的资源文件有唯一的 hash 值
-
修改后只有受影响的文件 hash 变化
- 增量式进行更新
hash
整个应用的 hash 值chunkhash
每个 chunk 的 hash 值(一个 chunk 拆分出来的css/js等文件,hash相同)contenthash
根据文件内容计算的 hash 值
-
充分利用浏览器缓存
监测与分析¶
-
Stats 分析与可视化图
- 由内向外看,每一部分代码,由哪些模块组成
-
每个 bundle 文件的体积分析
- 工具一: 安装 source-map-explorer(推荐)
- 工具二: 安装 webpack-bundle-analyzer
-
speed-measure-webpack-plugin 速度分析
- 统计 webpack 每个阶段耗时
-
示例
# 1. Stats 分析与可视化图
webpack --profile --json > stats.json
# 可视化结果 https://alexkuz.github.io/webpack-chart/
# 2. bundle 文件的体积分析
source-map-explorer 'dist/*.js'
# 必须配置source-map, 比如: devtool: 'hidden-source-map'
React 按需加载¶
- React router 基于 webpack 动态引入
- 使用 Reloadable 高级组件
传输加载优化¶
Guideline¶
- Gzip
- KeepAlive
- HTTP 缓存
- Service Worker
- HTTP/2
- SSR
- Nginx
Gzip 压缩¶
- 对传输资源进行体积压缩,可高达 90%
- 配置 nginx 启动 Gzip
- 示例
# nginx.conf
http {
gzip on; # 开启 gzip 压缩
gzip_min_length 1k; # 至少 1k 才进行压缩
gzip_comp_level 6; # 压缩级别 1 ~ 9,压缩比率越高,越消耗cpu
gzip_types text/plain text/html application/javascript text/css text/javascript application/json; # 压缩的资源类型
gzip_static on; # 压缩的静态资源,直接复用
gzip_vary on; # 告诉客户端启用了 gzip 压缩;增加响应头 "Vary: Accept-Encoding"
gzip_buffers 4 16k; # 优化压缩过程
gzip_http_version 1.1; # http 版本
server {
# TO DO
}
}
启用 KeepAlive¶
-
一个持久的 TCP 连接,节省连接创建时间
- waterfall 里的
initial connection
- waterfall 里的
-
nginx 默认开启 keepalive
# nginx.conf
# keepalive-timeout 0; # 不启用
keepalive-timeout 65; # 超时时间
keepalive-requests 100; # 保持连接的请求数
// 响应头 Response Headers
Connection: close // 表示没开启 keepalived
Connection: keepalived // 表示开启 keepalived
HTTP 缓存¶
- 目的:提高重复访问时资源加载速度
- Cache-Control/Expires
- Last-Modified + If-Modified-Since
- Etag + If-None-Match
- 示例
# nginx.conf
http {
server {
listen 9999;
server_name localhost;
location / {
root "/Users/leo_0/Studyspace/Learn to Code/Simple Demos/learning-demo-2-vue/dist";
index index.html index.htm;
try_files $uri /index.html;
if ($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "no-cache, must-revalidate";
add_header "Pragma" "no-cache";
add_header "Expires" "0";
}
if ($request_filename ~* .*\.(?:js|css)$)
{
expires 7d;
}
if ($request_filename ~* .*\.(?:jpg|jpeg|gif|png|svg|mp4|ogg)$)
{
expires 7d;
}
}
}
}
-
HTML 文件 Response Headers
- Cache-Control: no-cache, must-revalidate
- Expires: 0
-
JS 文件 Response Headers
- Date: Tue, 13 Oct 2020 03:55:49 GMT
- Expires: Tue, 20 Oct 2020 03:55:49 GMT
Service Workers¶
- 加速重复访问
- 离线支持
-
注意:
- 延长首屏时间,但页面总加载时间减少
- 只能在 localhost 或 https 下使用
HTTP/2¶
- 二进制传输
-
请求响应多路复用
- HTTP 1.1 虽然有 Keepalive 减少建立连接的开销,但资源依旧按顺序加载,相互阻塞
- HTTP/2 多路复用,真正意义上异步 or 并发 的请求资源
- 适合请求量大的页面,发挥较大优势
-
Server Push
- waferfall 中没有
TTFB 阶段
- 服务端直接 push 到浏览器,直接
Reading Push
,读取资源
- waferfall 中没有
-
示例
# nginx.conf 配置
server {
# listen 9443 ssl; # 不使用 http2
listen 9443 ssl http2; # 使用 http2
server_name localhost;
ssl on;
ssl_certificate ;
ssl_ssl_certificate_key ;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root "/Users/leo_0/Studyspace/Learn to Code/Simple Demos/learning-demo-2-vue/dist";
index index.html index.htm;
try_files $uri /index.html;
## 使用 server push 资源文件
# http2_push /img/pic0.jpg;
# http2_push /img/pic1.jpg;
# http2_push /img/pic2.jpg;
}
}
# 制作自签名证书
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
## 私钥
openssl rsa -passin pass:x -in server.pass.key -out server.key
## 证书请求文件(Certificate Signing Request)
openssl req -new -key server.key -out server.csr
## 证书
openssl x509 -req -sha256 -days 3650 -in server.csr -signkey server.key -out server.crt
- 图示:HTTP 1.1 vs HTTP2
- Tip: Chrome 访问不安全 HTTPS 口令:
thisisunsafe
SSR¶
- 加速首屏加载
- 更好的 SEO
- Case: 基于 Next.js 实现 SSR
- 图示:CSR vs SSR
其他优化技术¶
Guideline¶
- SVG 优化图标
- FlexBox 布局
- 预加载
- 预渲染
- 窗口化提高列表性能
- 骨架组件
SVG 优化图标¶
- 保持图片能力,支持多色彩
- 独立的矢量图
- XML 语法,搜索引擎 SEO 和无障碍读屏软件读取
FlexBox 布局¶
-
更高性能的实现方案
- 比较
float: left;
与display: flex;flex-flow: row wrap;
;查看 performance 发现Rendering & Painting
时间明显缩短。
- 比较
-
容器有能力决定子元素的大小,顺序,对齐,间隔等
- Flex container 容器
- Flex items 子元素
优化资源加载顺序¶
- 图示
- 浏览器默认安排资源加载优先级 Priority
-
使用 preload, prefetch 调整优先级
- Preload: 提前加载较晚出现但对当前页面非常重要的资源,优先级高
- Prefetch: 提前加载后继路由需要的资源,优先级低
-
示例
<!-- Preload Case -->
<link rel="preload" href="main.js" as="script">
<script src="main.js"></script>
<!-- Prefetch Case -->
<!-- index.html -->
<link rel="prefetch" as="style" href="style.css" >
<!-- page-1.html -->
<link rel="stylesheet" href="style.css" />
<!--
查看响应
Request Method: GET
Status Code: 200 OK (from prefetch cache)
-->
预渲染页面¶
-
use react-snap with Vue.js/React.js
- 页面攫取,类似爬虫,将爬取的结果拼装到页面
- 配置 postBuild
- 使用 ReactDOM.hydrate
- 内联样式,避免 FOUC(样式闪动)
-
作用:
- 大型 SPA 应用的性能瓶颈:JS 下载 + 解析 + 执行
- SSR 的主要问题:牺牲 TTFB 来补救 First Paint; 实现复杂
- Pre-rendering 打包时提前渲染页面,没有服务端参与
Windowing 窗口化¶
-
react-window
窗口化提升列表渲染性能- 始终让 DOM 保持比较小的体积
- 不同于 Lazy Loading
-
加载大列表,大表单的每一行严重影响性能
- Lazy Loading 仍然会让 DOM 变得过大
- Windowing 只渲染可见的行,渲染和滚动的性能都会提升
骨架组件¶
- 使用骨架组件减少布局移动(Layout Shift,
Shift + Command + P
) -
Skeleton/Placeholder 的作用
- 占位
- 提升用户感知性能
-
react-placeholder
习题¶
Web 加载&渲染原理¶
- 问题: 从输入 URL 到页面加载显示都发生了什么?
-
UI Thread
- 判断: 搜索 or URL? -> 搜索引擎 or 请求的站点
- UI Thread 会通知 Network Thread 发起请求
- 图示
-
Network Thread
- UI Thread & Network Thread 属于 browser process
- 获取 HTML 资源后,通信 Renderer Process
- 图示
-
Renderer Process
- 图示
首屏加载优化¶
- 问题:什么是首屏加载?怎么优化?
-
首屏重要指标
- FCP、LCP、TTI
- 图示
-
解决办法
-
资源体积太大?
- 资源压缩,传输压缩,代码拆分,Tree shaking,HTTP/2,缓存
-
首页内容太多?
- 路由/组件/内容 lazy-loading,预渲染/SSR,Inline CSS
-
加载顺序不适合?
- prefetch, preload 调整加载的优先级
- preload: 当浏览器解析到 preload 标签就会去加载 href 中对应的资源但不执行,待到真正使用到的时候再执行。
- prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源。
-
Javascript 内存管理¶
- 问题:JS 是怎样进行内存管理?什么情况会造成内存泄露?
-
内存回收
- 局部变量,函数执行完,没有闭包引用,会被标记回收
- 全局变量,直到浏览器被卸载页面时释放
-
GC 机制
-
引用计数
- 无法解决循环引用问题
-
标记清除
- 从 GC Root 开始扫描,所有引用是否能被访问到
- 如果不可访问,标记,随后回收
-
-
避免内存泄漏
- 避免意外的全局变量
- 避免反复运行引发大量闭包
- 避免脱离的 DOM 元素
- 示例
/* * case 1. 避免意外的全局变量 */ function accidentalGlobal() { leak1 = 'leak1'; this.leak2 = 'leak2'; } accidentalGlobal(); // window.leak1; // window.leak2;
/* * case 2. 避免反复运行引发大量闭包 */ var store; function outer() { var largeData = new Array(10000000000); var prevStore = store; function inner() { if (prevStore) { return largeData; } } return function() {} } setInterval(function() { store = outer(); }, 10)
/* * case 3. 避免脱离的 DOM 元素 */ function createElement() { const div = document.createElement('div'); div.id = 'detached'; return div; } const detachedDiv = createElement(); // 此处一直有引用,所以无法回收 document.body.appendChild(detachedDiv); function deleteElement() { // 从文档树中删除之后,并没有被回收 document.body.removeChild(document.getElementById('detached')); } deleteElement();