总览:
性能调优工具
chrome devtool: Network
能力: 查看网络请求以及资源加载的耗时
- Queueing 浏览器将资源放入队列时间
- Stalled 因放入队列时间而发生的停滞时间
- DNS Lookup DNS解析时间
- Initial connection 建立HTTP连接的时间
- SSL 浏览器与服务器建立安全性连接的时间
- TTFB 等待服务端返回数据的时间
- Content Download 浏览器下载资源的时间
chrome devtool: Lighthouse
- First Contentful Paint 首屏渲染时间,1s以内绿色
- Speed Index 速度指数,4s以内绿色
- Time to Interactive 到页面可交换的时间
chrome devtool: Performance
专业的网站性能分析工具
webPageTest
可以模拟不同场景下访问的情况, 比如不同的浏览器, 不同的国家等等.
webpack-bundle-analyzer
资源打包分析工具
前端性能参数
可以通过如下方法获取前端页面加载的时间:
window.addEventListener('DOMContentLoaded', (event) => {
let timing = performance.getEntriesByType('navigation')[0];
console.log(timing.domInteractive);
console.log(timing.fetchStart);
let diff = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + diff);
})
复制代码
更多的性能参数如下:
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数: redirectCount
重定向耗时: redirectEnd - redirectStart
复制代码
资源优化
图片资源优化
-
图片资源转CDN
把图片资源转存CDN有几个好处:
第一, 我们知道浏览器中的同域网页并发数量是受到控制的, 其并发限制如下:
把图片资源转存CND可以让图片请求不与主域抢网络资源
第二, 目前很多CDN可以动态裁剪和压缩图片, 这样在网站中就可以按照实际的显示尺寸加载对应分辨率的图片, 防止前端对图片进行缩放.
不要对图片进行了缩放的理由有两个:
- html缩小图片只缩小了尺寸, 图片会失真
- 缩放意味着图片大小不合适, 网页加载开销会偏高
-
雪碧图(CSS sprite)
雪碧图的目的也是减少http网络请求的数量, 原理不再赘述, 此外雪碧图一定程度上可以减小图片的总大小
-
图片懒加载
用js判断当该图片在页面中可见的时候再设置src值
-
响应式图片
除了控制cdn参数动态裁剪图片意外, 还可以用原生的响应式图片来在不同的环境下切换不同的图片资源:
<picture> <source srcset="banner_w1000.jpg" media="(min-width: 801px)"> <source srcset="banner_w800.jpg" media="(max-width: 800px)"> <img src="banner_w800.jpg" alt=""> </picture> 复制代码
-
压缩图片的大小
压缩图片的大小, 不需要透明背景就把png图片转化为jpg, 并且通过无损压缩可以减少图片的体积
在允许的情况下, 使用webp格式的图片能进一步压缩图片的大小
-
对于简单效果, 用css替代图片
资源预加载
浏览器渲染的流程, 可以参考本站浏览器原理
preload
在标签上添加preload
, 这些资源会在页面加载的生命周期的早起阶段就开始获取. 而不是等到具体渲染的时候
详细的草案可以看这里
<link rel="preload" href="style.css" as="style as="..." onload="preloadFinished()">
<link rel="preload" href="main.js" as="script as="..." onload="preloadFinished()">
复制代码
preload的特点如下:
- 将加载和执行分离, 提前加载, 在需要的时候执行
- 不论资源是否需要, 都会进行加载
- perload有as属性, 可以设置正确的资源加载优先级, 比如
as="style"
会获得最高的优先级, 设置as="script"
会获得低或者中优先级 - 可以定义资源的onload事件
- 对于跨域资源进行preload的时候, 必须添加
crossorigin
属性 - preload字体如果没有crossorigin会进行二次获取, 字体文件会被下载两次
- 没有用到的preload资源在chrome的console中会在onload后的3s后发生警告
- preload和HTTP2主动推送
http2在服务端获取到html文件时就知道需要对应的资源, 因此会直接向客户端推送, 而perload会在浏览器接收到html文件的时候才开始扫描这些预加载文件.
但是HTTP2不能用于第三方的资源推送, 而且preload有益于浏览器确定资源加载的优先级
prefetch
prefetch 是告诉浏览器下一页可能会用到的资源, 用于加速下一个页面的加载速度.
<link rel=“prefetch”>
复制代码
在vue ssr生成的页面中, 首页的资源都会使用preload, 而路由对应的页面则会有prefetch
注意, prefetch和preload不要混用, 会造成重复加载资源.
字体压缩
这里主要介绍两个工具吧,
一个是font-spider
, 可以自动检测网页中引用的字体和文字, 来生成字体文件.
一个是fontmin
, 可以将一个字体文件最小化, 比如:
var Fontmin = require('fontmin');
var fontmin = new Fontmin()
.src('fonts/Microsoft Yahei.ttf') // 设置服务端源字体文件
.dest('build/fonts') // 设置生成字体的目录
.use(Fontmin.glyph({
text: '字体压缩', // 设置需要的自己
}));
fontmin.run(function (err, files) { // 生成字体
if (err) {
throw err;
}
console.log(files[0]); // 返回生成字体结果的Buffer文件
});
复制代码
fontmin提供了webpack插件, 详细的使用说明可以参看这里
网络优化
静态资源使用CDN
内容分发网络(CDN)就是一组分布在多个不同地理位置的Web服务器.
CDN原理如下:
- 当一个用户访问一个网站的时候, 浏览器要经过DNS解析, 然后浏览器想目标服务器发出IP请求并得到资源
- 如果网站部署了CDN, 浏览器进行DNS解析
- DNS一次想根服务器, 顶级域名服务器, 权限服务器发出请求, 得到全局负载均衡系统(GSLB)的IP地址
- 本地DNS向GSLB发出请求, GSLB根据本地的DNS的IP地址判断用户的位置, 筛选出距离用户比较近的本地负载均衡系统(SLB), 并将该SLB的IP地址作为结果返回给本地的DNS
- SLB根据浏览器请求的资源和地址, 选择最优的缓存服务器将内容发回给浏览器
- 缓存服务器查看是否命中资源, 如果没有命中, 就向源服务器发送请求, 再发给浏览器并缓存在缓存服务器中
更多关于CND原理的描述参看CDN原理
减少HTTP请求(针对http1.1)
一个HTTP请求会消耗比较大的资源, 一旦你body中传输的数据很少, 头部和协议的解析消耗的资源占比就会增加.
我们可以缓存ajax请求, 对重复的请求直接从缓存中获取, 可以减少http的请求数量.
使用Http2
HTTP2相比http1.1有很多的优势, 比如解析速度快, 多路复用, 头部压缩, 能够进行服务端推送, 能够控制流量和优先级
具体协议内容参考: 计算机网络/HTTP2
优化Cookie的使用
Cookie的优点是兼容性好, 可以在不出参数的情况下和后台进行数据交互, 比如自动登录
缺点是:
- IE老浏览器会显示Cookie的数量
- 域名设置不当会导致所有请求都带上Cookie信息
- Cookie的读写性能非常的差
优化的方式如下:
- 尽可能减少网站中使用的cookie大小
- 给cookie设置合理的过期时间
- 静态资源不使用cookie, 进行cookie隔离
减少DNS查询
DNS负责将域名URL转化为服务器主机IP
DNS查找流程:
- 查看浏览器缓存是否存在
- 查看本机DNS缓存
- 访问本地DNS服务器
- …
通常浏览器查找一个给定URL的IP地址需要花费20~120ms
TTL(Time to Live) 表示查找返回的DNS记录包含的一个存活之间, 过期则该DNS被抛弃
DNS缓存的TTL值有几个影响因素:
- 服务器可以设置TTL表示DNS记录的存活时间. 本机DNS缓存将根据这个TTL值判断DNS记录什么时候被抛弃. TTL不能设置很大, 因为存在快速故障转移的问题
- 浏览器DNS缓存也有自己的过期时间, 该事件独立于本机DNS缓存, 相对比较daunt, 例如chrome只有一分钟
- 浏览器DNS记录的数量是有限制的, 如果短时间访问大量不同域名的网站, 则比较早的DNS记录会被抛弃
因此, 针对DNS优化, 我们需要恰当的减少主机域名的数量. 但是过少的主机名会限制并行下载的数量(注意: 针对http1.1), 比较恰当的数量是2-4个主机名能够获取加大收益
避免重定向
重定向用于将用户从一个URL重新路由到另一个URL, 一般有:
- 301: 永久重定向
- 302: 临时重定向
- 304: Not Modified
详情参照HTTP1.x
页面发生重定向会延迟真个HTML文档的传输, 增长白屏的时间.
那什么时候会用到重定向呢:
- 跟踪内部的流量: 用户离开主页之后的流量, 但最好用内部的referer日志来跟踪内部流量
- 跟踪出站的流量: 比如某些链接会出站, 我们可以将其包装在一个302的重定向连接中来解决跟踪的问题
启用 Gzip
Gzip能更进一步的压缩前端资源文件的大小.
但是不是每个浏览器都支持gzip的, 可以在请求头中配置accept-encoding
来表示对压缩的支持, 客户端http请求头声明浏览器支持的压缩方式, 服务端配置启用压缩, 压缩的文件类型, 压缩方式, 当客户端请求到服务端的时候, 服务器解析请求头, 如果客户端支持gzip, 响应的时候就会进行资源的压缩并返回给客户端. 浏览器按照自己的方式解析.
如何启用Gzip这里不再赘述
缓存控制
浏览器缓存可以参照缓存策略
- 频繁变动的资源:
Cache-Control: no-cache
- 不常变化的资源:
Cache-Control: max-age=31536000
具体的内容在这里就不展开了
构建优化
目前主流的前端项目都是有构建过程的, 其中有许多优化技巧
tree shaking
tree shaking
意为将js文件中用不到的代码在打包的过程中删除.
webpack2以上以及rollup都能很好的支持该特性
无用代码消除广泛存在于编程语言编译器中, 称为DCE(dead code elimination)
其大致的实现原理简单的概括就是:
- ES6 Module引入进行静态分析, 编译的时候就可以正确判断加载了哪些模块
- 静态分析程序流, 判断哪些模块和变量未被使用或者引用, 进而删除对应的代码
在webpack中, 可以通过在package.json
中配置sideEffects
表示可以进行treeshaking:
{
"name": "your-project",
"sideEffects": false
}
复制代码
或者表示那些文件具有副作用:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}
复制代码
而对于rollup
, 其默认支持treeshaking
代码压缩
webpack等打包插件可以会生产环境的代码进行压缩, 能够减小资源文件的大小, 优化前端的加载性能.
压缩能力目前都内置在打包工具中, 只要开始生产环境配置即可.
比如webpack配置mode: 'production'
就可以开启压缩:
module.exports = {
mode: 'production'
};
复制代码
rollup则需要安装对应的插件, 比如terser
:
import { terser } from "rollup-plugin-terser";
export default {
plugins: [
terser({ compress: { drop_console: true } })
]
};
复制代码
打包分离(Bundle Splitting)
打包分离的思想是: 如果你有一个体积巨大的文件, 并且只修改了一行代码, 用户却仍然需要重新下载整个文件, 但是如果我把它分成了两个文件, 那么用户只需要下载那个被修改的文件, 而另一个文件直接从缓存中获取就可以了.
从这个角度看, 打包分离与缓存相关, 所以对站点的首次访问者来说没有区别.
webpack可以简单的配置:
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
复制代码
结果会生成一个main.js和一个vendor.js把第三方类库进行分离. 这个配置意味着: 把所有node_modules里的东西都然道verndors~main.js文件中去.
或者我们可以进行这样的配置:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
plugins: [
new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
};
复制代码
这里我们只是简单的介绍打包分离的思想和简单的使用示例, 详细的原理和细节可以参考这篇文章: 深入理解WebPack打包分块
按需加载
按需加载和按需架子啊是两个意图不同的事情, 前者的意图参见上一小节, 按需加载目的是在用户首次访问的时候能尽量减少加载的文件大小, 对暂时不需要用到的代码进行动态加载.
比如这段代码:
window.document.getElementById('btn').addEventListener('click', function () {
// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
import(/* webpackChunkName: "show" */ './show').then((show) => {
show('Webpack');
})
});
复制代码
当 Webpack 遇到了类似的语句时会这样处理:
- 以
./show.js
为入口新生成一个Chunk - 当代码执行到import所在的语句是会去加载有chunk对应生成的文件
- import返回一个promise, 当文件加载成功的时候可以在
promise
的then
方法中获取到show.js
导出的内容.
/* webpackChunkName: "show" */
的含义是为动态生成的 Chunk 赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的 Chunk 的名称,默认名称将会是[id].js
Dllplugin 提升构建速度
DLLPlugin 和 DLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度。
具体使用参考webpack-DllPlugin
ssr:服务端渲染
目前vue/react生成的前端项目, 其页面视图都是通过js动态生成的, 并且往往前端需要加载一个复杂的rumtime运行库. 对于首屏的渲染来说会比较慢. ssr就是把这部分的渲染过程放在服务端, 请求页面的时候就直接读取dom内容.
不但利于首屏渲染,也有利于SEO
缺点在于会增加服务端的压力, 并且会有一定的改造成本
代码优化
HTML性能优化
HTML的优化主要是规范化标签的使用, 比如:
- HTML标签始终闭合
- script移到html文件的末尾, 因为JS会阻塞后面的页面的显示
- 减少iframe的使用
- 简化id和class
- 保持统一的大小写
- 清除空格
- 减少不必要的嵌套
- 减少注释
- 去除无用的标签和空标签
- 减少使用废弃的标签
- 避免空的
img:src
CSS性能优化
CSS性能是个比较广泛的东西, 主要从四个方面:
- 加载性能, 主要是从减少文件体积, 减少阻塞加载, 提高并发出发的
- 选择其性能, 但实际上对整体性能的影响忽略不计, selector的考察主要是规范化, 可维护性, 健壮性方面. 可以参考这篇文章: githubs-css-performance
- 渲染性能. 渲染性能是css优化的最重要的专注对象. 页面渲染junky过多, 看看是不是使用了text-shadow, 是不是开了字体抗锯齿, CSS动画的实现, 是否合理的使用了GPU加速.
- 可维护性, 健壮性. 命名是否合理, 结构层次设计是否健壮, 样式抽象复用了吗
- 减少重绘和回流
JS 性能优化
JS性能优化的方向就更多了
从工程角度讲:
- 删除没有使用到的功能性代码
- 删除多余的依赖库
- 删除公共模板代码
从使用内存角度讲:
- 数组和对象避免使用构造函数
- 避免使用非必要的全局变量
- 合理的使用JS缓存机制, 即本地的loaclStorage, SessionStorage, cookie等
- 减少循环中的代码实例
- 减少比必要的变量声明
- 注意闭包的使用, 不要造成内存泄露
- 长列表优化
- 避免js运行时间过长, 合理的分解任务, 延迟执行高消耗的任务
- 利用好web worker, service worker等API
- 使用wasm
- 使用函数防抖/节流, 尾递归等优化技巧
Vue 性能优化技巧
- 使用函数式组件
- 子组件拆分
- 使用局部变量
- 用
v-show
代替v-if
- 使用
keepalive
缓存DOM - 使用
deferred
组件延时分批渲染组件 - 使用
time slicing
时间片切割技术 - 合理使用非响应式数据
- 使用虚拟滚动组件
……
React 性能优化技巧
这部分的优化可以查看这里: React 性能优化
尾声
性能优化深无止境, 我这篇短文就只能列举其中的部分能用并介绍一二, 甚至不能进行比较深入的讨论, 因为那样就实在太长了. 多积累, 多总结, 总能让技术越来越好的.