前端优化范围往大了分可分为两种:网络优化和渲染优化
网络优化:从Url输入到页面渲染
主要有三个过程:DNS 解析 -> TCP 连接 -> HTTP 请求/响应
对于DNS解析和TCP连接前端可以优化的地方有限,所以主要专注HTTP优化。
而HTTP优化可分为两个大致方向:
- 减少请求次数
- 减少单次请求时间
以上两个方向直接指向了日常开发中非常常见的操作,资源的压缩和合并----webpack
babel-loader : exclude:/(node_modules|bower_components)/ 跳过文件夹,以及添加参数 loader: 'babel-loader?cacheDirectory=true' 缓存转译结果至文件系统。
dllPlugin&&dllReferencePlugin结合处理第三方依赖,因为第三方依赖不会变动,只需打包一次,避免在后续开发中重复构建,影响开发效率
//dllModel.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry:{
//依赖的库数组
vendor:['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
library: '[name]_[hash]',
},
plugins:[
new webpack.dllPlugin({
name:'[name]_[hash]',
path:path.join(__dirname,'dist','[name]-manifest,json'),
context:__dirname
})
]
}
运行webpack --config dllModel.config.js 后会在dist文件夹下面生成vendor-manifest.json和vendor.js,分别是预编译包配置和文件,配合主包使用如下配置:
//webpack.prod.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
// 编译入口
entry: {
main: './src/index.js'
},
// 目标文件
output: {
path: path.join(__dirname, 'dist/'),
filename: '[name].js'
},
// dll相关配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest就是我们第一步中打包出来的json文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
此后打包时都会绕过vendor-manifest.json内预编译好的第三方包。
webpack是单线程的,就算存在多个任务也只能一个接一个等待处理,但我们机器cpu是多核的,因此可以将任务分解给多个子进程并发执行,大大提高打包效率,happypack-将loader由单线程转为多线程
const HappyPack = require('happypack');
const os = require('os');
const HappyThreadPool = HappyPack.ThreadPool({size:os.cpus().length});
module.exports = {
module:{
rules:[
...,
{
test:/\.js$/,
loader:'happypack/loader?id=happyBabel',
}
]
},
plugins:[
...,
new HappyPack({
id:'happyBabel',
//指定进程池
threadPool:HappyThreadPool,
loaders:['babel-loader?cacheDirectory']
})
]
}
webpack4 对于mode:'production'启用了treesharking去除了未被引用的模块,默认支持uglifyjs-webpack-plugin处理粒度更细的冗余代码
const UglifyPlugin = require('uglify-webpack-plugin')
module.exports = {
plugins:[
...,//其他配置
new uglifyPlugin({
//允许并发
parallel:true,
//开启缓存
cache:true,
compress:{
//去除所有console
drop_console:true,
//把多次定义的静态值设置为变量
reduce_vars:true
},
output:{
//不保留注释
comment:false,
//使输出的代码尽可能紧凑
beautify:false
}
})]
}
按需加载:页面之间不存在相互依赖的关系,不需要一次性加载完成,只加载页面需要的文件,本质上是减少了页面渲染需要加载的文件,减少了http请求次数和单次请求的时间
require.ensure(dependencies, callback, chunkName)引入组件
output: {
path: path.join(__dirname, '/../dist'),
filename: 'app.js',
publicPath: defaultSettings.publicPath,
// 指定 chunkFilename
chunkFilename: '[name].[chunkhash:5].chunk.js',
},
对于有一定规模大小的文件启用gzip压缩,可以有效压缩70%左右,只需要在请求的时候在头部加上 accept-encoding:gzip,这样会增加服务器的消耗,而webpack本身启用gzip压缩js,css等文件,原理上就是对代码重新编码,用更少的字节替换。
图片的权衡
图片是前端页面最重要的部分,前端优化首先不是提高js,css下载速度或者减少下载时间,而是尽可能快的让图片和文字展示给用户。
时下web的图片格式大致有jpg/jpeg,png,svg,webp,base64,像素是由二进制来表示,不同格式的图片所能展示的颜色数量不同,如果一个图片格式支持n位二进制,那么它可以支持2^n种颜色
jpg/jpeg:体积小,加载快,有损压缩,24位,不支持透明
应用场景:使用jpg/jpeg格式的图片来呈现大图,既可以保住图片的质量又可以避免体积过大,是时下banner图,背景图的选项
不适合场景:logo和矢量图形等颜色对比强烈,线条感强的场景
png:无损压缩,质量高,体积大,支持透明,分为 8 位和24位
应用场景:适用于颜色简单,对比度强,通常应用logo
svg:矢量图,无限放缩不失真,文本文件,体积小,兼容性好
基于xml语法的文件格式,对图像的处理不基于像素,而是对图形的描述,svg和jpg,png相比,体积更小,可压缩性更强,缺点是渲染成本比较高,并且可编程,需要一定学习成本。
base64: 编码格式,文本文件,小图标解决方案
像大量图标可以合并为一个雪碧图,从而较少http请求次数,而base64可以直接写入css或者html,减少了http请求次数。
缺点:使用base64编码图片后,文件大小会膨胀到原来的4/3,所以通常处理非常小,更新频率低的图片
webp:google在2010年专为web开发的图片格式,旨在**加快图片加载速度,**相比其他图片格式,webp支持有损和无损压缩,支持图片透明,压缩的体积更小,唯一缺陷是兼容性差,google 49及以上支持,大部分浏览器都不支持。
处于性能优化以及兼容,在用户请求图片资源时会头部携带Accept字段,如果包含image/webp,则返回webp格式的文件。
浏览器缓存
memory cache
Service Woker cache
HTTP cache
Push cache
http cache 分为强缓存和协商缓存,优先级高的时强缓存,命中强缓存失败的情况下才会触发协商缓存
强缓存是由http 头中的expires和cache-control 来控制的,初次请求会根据头部内容选择保留,再次请求时会根据之前保留的头部判断目标资源是否命中强缓存,若命中则直接从缓存中获取,不会再与服务端发生通信
expires 会相对于客户端本地时间来判断资源是否过期,再次请求资源时会比对本地时间和expires内的时间戳,但客户端和服务端有可能时间不一致,并且存在客户端修改时间导致资源过期。
cache-control:max-age=n 可以设置在多少秒内资源有效,s-maxage 优先级高于max-age,区别在于s-maxage设置的资源被代理服务器缓存的,针对public类型的资源。资源默认是private的,即只能被浏览器缓存,带有public特性则能被代理服务器缓存。
no-cache 只会走协商缓存,no-store 则每次请求都是下载最新资源。
协商缓存是浏览器和服务端进行通信,判断资源是否重新获取,一般是资源过期引发协商缓存,如果服务端提示资源没有发生改动,资源会被重定向浏览器缓存,并且状态码是304,
304 not modified 重定向。
判断资源是否变更最开始是判断Last-Modified,来判断,即资源的最后一次编辑时间,但存在编辑了文件,但内容没有改变,会导致重新请求,并且无法区分一秒内的多次修改,所以etag作为补充,相当于资源的唯一标识符,etag优先级高于last-modified,但会导致额外的服务端开销。
memory cache 指在内存中的缓存,响应速度最快,生命最最短,和渲染进程同时存在,当tab页关闭即释放,浏览器最先尝试命中的区域,一般base64格式的图片都能被塞进内存,也包括体积不大的js,css,本着节约原则
Service Worker cache 创建独立于主线程之外的js线程,本质上是浏览器和服务器之间的代理服务器,webworker的一种,它脱离于窗体,因此不能直接操作Dom,是的serviceworker 的行为无法干扰页面的性能,可以帮助我们实现离线缓存,消息推送和网络代理等功能。
serviceworkercache 的生命周期有 : install,active,working,servicework一旦被install,就会一直存在,然后在active和working之间切换。
window.navigator.serviceWorker.register('/test.js')注册子线程
//test.js
self.addEventListener('install',(event)=>{
event.waitUntil(
// 考虑到缓存也要更新,通过版本号控制
caches.open('test-v1').then(cache=>{
//需要缓存的文件列表
return caches.addAll(['./test.js','./test.css'])
})
)
})
//serviceworker可以监听所有的接口请求,监听fetch
self.addEventListener('fetch',event=>{
event.respondWith(
//查看是否命中缓存
caches.match(event.request).then(res=>{
// 如果命中了,会有返回
if(res)return res
//没命中就继续向服务端发起请求
return fetch(event.request).then(response=>{
if(!response||response.status !== 200) return response
//请求成功的话将结果缓存起来
caches.open('test-v1').then(cache=>{
cache.put(event.request,response)
})
return response.clone();
})
})
)
})
Push Cache 其他三种缓存都未命中才会考虑push cache,基于http2,不同页面共享同一个http2连接就可以共享同一个Push Cache.
除了缓存,本地存储也是优化的措施
Cookie,localStorage,SessionStorage,indexedDb
Cookie 的本职工作是为了维持状态,但只能存储4kb左右的内容,键值对形式,同一个域名下的所有请求都会附带cookie,对于一些不需要cookie的静态资源也会加上,这造成了额外的开销。
localStorage和sessionStorage在api上面是没有差异的,区别在于localStorage是持久的,sessionStorage只能是同一个窗口下共享,大约能存储5~10M的内容。可以用来存储一些base64的图片。
IndexedDB 运行在浏览器上的非关系数据库,一般没有大小限制,大于250M,如果设计本地存储无法解决的程度,则可以使用indexedDB。
CDN 的实际应用
静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段,在许多一线的互联网公司,“静态资源走 CDN”并不是一个建议,而是一个规定。
原理是分布在全国各地的服务器,选择离用户最近的一个,同时会对过期的资源进行回源,即重新获取,保证资源的即时性。
选择和业务域名不同的cdn服务器,这样可以避免携带cookie。
服务端渲染
对于一些文章或者内容来获取流量的网站推荐使用ssr(服务端渲染),优化首屏加载速度,因为本来需要客户端处理的事交由服务端处理了,同时对搜索引擎友好,搜索引擎不会执行页面的js,因此SSR天然可以更好的被爬虫录取。原理就是利用框架的render模块将vnode转化为真实dom,现在node上跑一遍。
服务端渲染缺点就是吃服务器资源,因为需要让服务器去运行render,单核cpu只能支持几十甚至十几的QPS(每秒请求数)。
CSS优化
因为css引擎查找样式表的规则是从右往左,因此,更少的层级能带来更快的处理速度,多用id选择器,少用标签选择器,利用class提取可复用的css。
css是阻塞的资源,即便dom解析完毕了,也需要等待css解析完毕,主要是避免没有样式页面过于丑陋,因此要尽可能早的解析完css,所以有将css资源引用放到head内,或者CDN加速。
JS优化
在不做声明的情况下,js也是阻塞的,当HTML解析器遇到script标签时会暂停渲染,把控制权交给js引擎,js引擎对内联js会立即执行,对外部脚本需要先获取后执行,等到js执行完毕,浏览器才会将控制权交还渲染引擎。
但是我们会知道这个脚本何时执行,因此可以让它异步,从而不阻塞渲染进程。
async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。
<script async src="./test.js"></script>
defer 模式下,JS不会阻塞浏览器做其它事情,加载异步,当它加载结束时,需要等到整个文档解析完毕,DOMcontentLoaded事件即将被触发时,才会执行有defer标记的js文件。
<script defer src="./test.js"></script>
回流和重绘
每次进行Dom操作的时候都会通过交接接口去修改渲染引擎内的渲染树,对于批量Dom操作,可以将每次改动的数据缓存下来,批量处理完成后再去操作真实Dom,或者利用Fragment创建独立于真实DOM树的最小文档对象,来缓存批量化的DOM操作,document.createDocumentFragment创建一个新的容器,允许我们在这个容器内进行任意的Dom操作,而不用担心重绘和回流。
对于“离线”的DOM进行操作不会触发回流和重绘,原理上就是dispaly:none,将dom离线,操作完毕后再display:block,将dom显示。
事件循环
事件循环中异步队列有两种:macro(宏任务),micro(微任务)
常见的macro:setTimeout,setInterval,setImmediate,script(整体代码),I/O操作,UI 渲染等。
常见的micro:process.nextTick,Promise,mutationObsever等
一个完整的Event Loop 可以概括为下列过程:
初始状态:调用栈为空,micro队列空,macro内有一个script脚本(整体代码)
全局上下文,script被推入调用栈,同步代码先执行,执行过程中会产生新得macro和micro推入对应的队列,执行完后,script被移出队列,本质上就是队列的宏任务执行的出列的过程。
然后处理micro-task,执行macro是一个一个执行,而micro是一队一队执行,只有当前micro队列清空,才会执行下一步。
执行渲染操作,更新界面。(每当微任务执行完毕都会执行更新界面的操作,因此修改Dom的操作都应放在micro队列内,减少渲染次数)
检查是否存在web woker ,存在就执行。
懒加载,节流和防抖
节流防抖监听scroll事件,当页面高度达到指定值时请求新得数据。
在规定间隔内多次触发,会重新计时,超过间隔时间立即执行。
function throttle (fn,delay = 1000){
//laster 为上次触发的事件,timer为计时器
let laster = 0, timer = null
return function (){
let context = this,args = arguments,now = +new Date();//等同于new Date().getTime()
//判断上次触发间隔是否小于delay
if(now - last < delay){
clearTimeOut(timer);
timer = setTimeOut(function(){
last = now;
fn.apply(context,args)
},delay)
}else{
last = now;
fn.apply(context,args)
}
}
}