一、webpack方向
webpack优化其实可以归为HTTP层面的优化(网络层面)。因为HTTP这一层的优化两大方向就是:减少请求次数
和缩短单次请求所花费的时间
。而这两个优化点的手段就是“资源的合并与压缩”,正是我们每天用构建工具在做的事情,webpack无疑是最主流的。
webpack的优化主要是两个主题:构建过程时间太长
和打包的文件体积太大
。
(1)构建过程时间太长这个问题,因为不涉及产品体验层面,所以不被重视很正常,打包慢的原因多是因为重复打包,第三方库(以node_modules为代表),CommonsChunkPlugin每次构建时都会重新构建一次vendor,而平时一般从搭建工程开始这些第三方库就基本不会动,不用每次打包时都从头构建一遍,浪费时间。(补充:vendor.js文件是所有第三方库打包合并的结果,比如vuecli项目最终会在dist下打包生成一个chunk-vendors.js)。推荐用DllPlugin工具,它会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库,这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化才会重新打包。(其他的工具比如用happyPack将loader由单进程转为多进程加快速度就不多说了,补充下webpack是单线程的)
(2)打包的文件体积太大这个问题,有三个方面可以优化:按需加载、清除冗余代码、用Gzip压缩。按需加载就不说了,清除冗余代码从webpack2开始就推出了tree-Shaking,它可以在编译的过程中获悉哪些模块并没有真正被使用,这些没用的代码,在最后打包的时候会被去除。比如一个js文件export出了2个函数,其中一个函数没有在任何地方import使用过,打包时tree-shaking就会把它删掉。到了webpack4就内置了uglifyjs-webpack-plugin对代码做压缩了,它会删掉一些console语句、注释等一些对开发者有用而对用户无用的代码。
二、Gzip方向
Gzip压缩背后的原理是在一个文本文件中找出一些重复出现的字符串,临时替换他们,来使整个文件变小,所以比较适合文本文件,不适合图片,所以文件中代码的重复率越高,压缩率就越高,使用Gzip的收益就越大。通常Gzip压缩率能达到60%~70%,尤其是上面讲的第三方库打包合并成vendor.js文件,体积就不小。Gzip压缩可以是前端打包构建的时候做,也可以服务端实时地去压缩,我做过的项目通常都是我前端去压缩。压缩/解压就是牺牲服务器压缩的时间和浏览器解压的时间去换网络传输的时间
。我做过的项目一般会设置10KB这么一个阀值,打包后大于10KB的js\css\html文件才需要压缩。如果打包出来的都是1K、2K的文件,就没必要压缩了。
简要说下Vue项目配置Gzip的方法:1、首先npm安装compression-webpack-plugin库,2、在vue.config.js里配置:
const CompressionPlugin = require("compression-webpack-plugin")
module.exports = {
publicPath: process.env.NODE_ENV === 'production'? '/canteen/': '/',
productionSourceMap:false, //不生成.js.map文件,加速构建速度
configureWebpack: config => { //webpack配置Gzip压缩
if(process.env.NODE_ENV === 'production'){
return {
plugins:[new CompressionPlugin({
test:/\.js$|\.html$|\.css$/,
threshold:10240, //超过10KB的才需要压缩
deleteOriginalAssets:false //是否删除原文件
})]
}
}
},
}
服务端也需要配置,当支持Gzip的浏览器发起http请求时,会自动在请求头带上accept-encoding:gzip,服务端就返回压缩后的文件,浏览器再解压缩。如果请求头没有这个就返回原文件。
服务端配置Gzip。有动态
和静态
两种。服务端动态gzip是常见的方案,即服务端判断浏览器http请求头中的Accept-Encoding是否有gzip,有的话就说明浏览器支持gzip,服务器就实时压缩生成gzip返回给浏览器,否则就返回原文件。但是这种模式是比较消耗服务器CPU的,如果前端打包的时候就压缩好,把原文件和gzip文件全丢到服务器上,服务器不干压缩的活,只区分浏览器是不是支持gzip,支持就返gzip文件,不支持就返原文件,那就能省去服务器动态压缩的环节。注意:因为Linux系统下nginx不能向磁盘写文件,所以服务端只能实时生成。另外,如果你公司分配给你的服务器数量少,就不要用nginx动态压缩了。
服务器nginx配置静态gzip方法:
在nginx.config的目标应用location下配置:
gzip_static on;
gzip_http_version 1.1;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
三、图片优化方向
图片的优化其实就是根据业务场景,在不同格式的图片之间做选择。当前广泛使用的图片格式有jpeg/jpg、png、svg、base64、webP,再加个雪碧图方案。我们的目标依然还是资源压缩
和减少HTTP请求
。
(1)jpeg/jpg
:有损压缩、体积小、加载快、不支持透明。这种格式最大特点就是有损压缩,但是即使被称为有损压缩,jpg仍然是一种高质量的压缩方式,当它的质量问题不被我们肉眼察觉,就没问题。JPG适合用于呈现色彩丰富的图片,背景图、轮播图、Banner图基本都会用jpg格式。淘宝京东首页大图都是jpg的。他也有缺陷,不支持透明是第一个、第二个是展示logo等线条感比较强的图形时模糊感比较明显。
(2)png
:无损压缩、质量高、体积大、支持透明。唯一的bug就是体积大,特点是支持透明。png-8和png-24,就是位数不一样,8位的最多支持2的8次方(256)种颜色,24位的最多支持2的24次方(1600万)种颜色,相应的体积也更大,一般场景用png-8足够了。一般logo、颜色简单、线条明显的图片用png。淘宝首页不论logo大小,全都用的png。
(3)svg
:文本文件、体积小、不失真、兼容性好。唯一bug是渲染成本比较高。svg文件的体积甚至比jpg更小,它最显著的特点是可以无限放大而不失真,一张svg足以适配n种分辨率。svg是文本文件,可编程,可以写到html里和html一起下载不占用HTTP请求。实际开发中,我们一般会把svg写入独立文件后再引入html:<img src="xxx.svg" alt=""> 如果设计师没有给我们svg文件,我们也可以用阿里的iconfont在线矢量图形库去做。
(4)base64
:文本文件、雪碧图的补充方案。base64也一样可以写到html里,不占用HTTP请求。base64编码后图片体积会比原来的大1/3,如果你页面图标很小,又很少,又不想合成雪碧图,就可以用base64。如果大图也用base64转码,比起省掉的HTTP请求,体积膨胀反而会带来性能开销。所以base64适合小图标。
(5)webP
:年轻的全能型选手。硬伤是很新,兼容性差。它是谷歌专为web开发的图片格式,目的是加快图片加载速度,所以只有chrome全程兼容。它支持有损压缩、无损压缩、支持透明、动图等等,如果兼容性解决了,是最合适的方案。兼容的话可以交给服务端,服务器根据HTTP请求头的accept字段来决定返回什么格式的图片。accept包含image/webp时就返回webp格式。
四、CDN方向
使用CDN目的是使http传输路径变短
。CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,服务器可以按照就近原则
把距离客户端最近的服务器上的资源返回。
浏览器缓存和本地存储带来的性能提升,是在“已经获取到资源了再存起来,方便下一次的访问”,只能加快二次请求的速度,而CDN可以缩短首次请求的时间。CDN服务器上一般只存静态资源。它的核心是是缓存和回源,缓存就是部署时把静态资源copy一份到CDN服务器上,回源就是CDN发现自己没有这个资源(一般是静态资源过期了),转头再向根服务器访问资源。
另外,一般CDN服务器和根服务器的域名会用不一样的,比如www.taobao.com它用的CDN服务器域名是g.alicdn.com。这样做有一个额外的好处,原本访问www.taobao.com带上的Cookie,在访问g.alicdn.com不会被带上,节省了开销,因为Cookie 是紧跟域名的,同一个域名下的所有请求,都会携带 Cookie,换种说法就是访问CDN获取静态资源时HTTP不会带上Cookie,而获取静态资源时也压根用不到Cookie。
五、服务端渲染方向
先说客户端渲染。是把HTML,JS这些都下载下来,在浏览器里跑一遍JS,根据JS的运行结果生成DOM。服务端渲染就是在服务端运行js,生成DOM,再把完整的html返回给浏览器。所以服务端也是实时渲染的,消耗服务器CPU,高并发是个瓶颈,所以如果不是服务器性能很强大,不会优先考虑服务端渲染,就算做,一般也只会给首屏页面做服务端渲染。
另外,很多网站做服务端渲染的首要目的也并不是性能向的考虑,而是出于SEO优化的目的。客户端渲染时,搜索引擎可不会去跑js,只会查找当前html静态的关键字。所以,一般都是其他性能优化的招数都用尽了再考虑服务端渲染。有人说:“ssr这个东西好是好,可是流量上来的话,即便优化的再好也是非常吃硬件资源的。虚拟运维的话1核CPU的容器也就能支撑几十甚至十几的QPS”。
六、HTTP2方向
http1.1的并行加载资源数为6到8个(准确的说是每个域最多只能同时建立6个链接)
http2的优点在于一个域只建立一次TCP连接,使用多路复用,同时传输的资源数几乎没有上限,这样就不用使用雪碧图、合并JS/CSS等方法了。如果没有正确配置,nginx会自动切换到HTTP/1.1,所以兼容性很好。
不需要开发,只需要服务端nginx配置:要求openssl版本高于1.0.2,然后修改nginx.config:listen 443 ssl 改为listen 443 ssl http2,重启nginx即可。
七、浏览器缓存(HTTP缓存)方向
HTTP缓存是指当下一次发HTTP请求资源前,判断下是否应该从客户端本地拿缓存。分为强缓存
和协商缓存
。如果命中强缓存,则客户端不再询问服务器而直接去客户端本地读取资源;如果命中协商缓存,就发HTTP询问服务器是否应该读本地的资源,服务器不会返回所请求的资源,而是告诉客户端可以直接从客户端本地加载这个资源,于是浏览器就又会从自己的缓存中去加载这个资源。当两种模式都没命中,浏览器直接从服务器加载资源。
所以,强缓存与协商缓存的共同点是:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;区别是:强缓存不发请求到服务器,协商缓存会发请求到服务器。
1、强缓存怎么做的:
先说早期的expire。早些年没有协商缓存的概念,缓存指的也是强缓存,一直用expire。expire的值是一个时间戳——expires: Wed, 11 Sep 2019 16:12:18 GMT,是服务器响应HTTP时返回给浏览器的,意思就是“我返回给你的文件资源,你要缓存起来,在这个时间节点前你都去读缓存,不要再问我要了”。所以expire有个bug,服务器返回的时间戳是服务器时间,而浏览器发HTTP前用来和expire比较的是本地时间,两个时间可能不一致。
再说cache-control。cache-control就是expire的升级版了,强缓存和协商缓存都由它控制。如果和expire同时出现,cache-control优先级更高。cache-control有很多种取值:
private
: 只有浏览器可以缓存,(默认值)
public
:可以被任何缓存区缓存,包括浏览器和代理服务器
max-age=xxx
: 缓存将在 xxx 秒后失效
s-maxage=xxx
: 代理服务上的缓存将在 xxx 秒后失效
no-cache
:需要使用协商缓存来验证缓存数据
no-store
: 所有内容都不会缓存,强制缓存和协商缓存都不会触发
max-age的值就是一个时间段了(36000秒这样),这是个相对时间,解决了expire的bug。max-age=xxx并不是独立的属性,如你设置了cache-control:max-age=1800,等价于cache-Control:private, max-age=1800。
s-maxage 不像 max-age 一样为大家所熟知。它用来表示代理服务器上的资源缓存期限,所以只对 public 缓存有效(如果写了s-maxage,pubic可以省略不写)。cache-control: max-age=36000,s-maxage=31536000,两者同时出现时s-magage优先级更高。
2、协商缓存怎么做的:
只要服务器响应的头里写上cache-control:no-cache,就表示浏览器下一次请求相同文件时需要使用协商缓存。
先说Last-Modified / If-Modified-Since。和expire一样last-modified是一个时间戳,浏览器第一次跟服务器请求资源文件时,服务器会在响应头写上last-modified表示文件最后修改的时间,浏览器要把这个文件和最后修改时间都缓存起来,下一次请求相同文件时,把这个时间写到if-modified-since字段上,发给服务器,服务器根据浏览器传过来if-modified-since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源文件。浏览器收到304后就从本地缓存加载资源。值得注意的是,如果服务器判断资源文件没变化时不会再往响应头里写last-modified字段了。这种方式在绝大多数情况是足够可靠的
,但一般文件系统能感知到文件修改的最小粒度是s,所以一些极限情况下,文件内容修改了修改时间不一定变。所以产生了ETag方式。
再说ETag / If-None-Match。ETag的值是根据资源文件内容生成的唯一标识字符串,比如:ETag: W/"2a3b-1602480f459",只要文件内容变化唯一标识就不一样。它的工作流程和“最后修改时间”基本一样:第一次请求资源文件时,服务器计算好ETag值并写在响应头,浏览器收到后把资源文件和ETag值缓存起来,下一次请求相同文件时,把这个ETag写到if-none-match字段上,发给服务器,服务器计算文件当前的ETag,比较浏览器传来的if-none-match。我们看到,Etag的计算是实时的,会影响服务端的性能,而且文件越大,开销越大。因此这个方案要谨慎使用,有的公司甚至直接不让用ETag方法。
Etag可以是ETag: W/"2a3b-1602480f459",也可以是ETag:"2a3b-1602480f459",以W/开头的表示weak tag(弱Etag,不带W/的就是强Etag)。
最后总结下协商缓存。大多数情况下,可以关掉ETag而只用Last-Modified。ETag是特殊情况下Last-Modified的补充,而不是替代。两者都存在的情况下ETag优先级比Last-Modified高。不管哪个方法,“最后修改时间”和“唯一标识”都由浏览器来存,服务器是不可能花费空间来保存缓存状态的。
3、浏览器把资源文件缓存在什么地方?
可以看到,浏览器会从3个地方读缓存:
from memory cache:从内存读缓存,最快也最短命,页面关闭,文件就没了。
from ServiceWorker:从浏览器的service worker读取缓存,service worker是一个浏览器中的进程而不是浏览器内核下的线程,因此它在被注册安装之后,能够被在多个页面中使用,也不会因为页面的关闭而被销毁。所以浏览器关了文件才销毁。
from disk cache:从浏览器客户端本地磁盘读缓存,浏览器关闭,磁盘文件还在。
至于缓存到哪里这个事不是标准规范,纯属浏览器个人行为。各家浏览器的策略不尽相同,比如chrome会把体积大的js、css放到磁盘,小的放内存;而火狐浏览器就只会存在内存里,没有from disk cache,不会放到磁盘。
八、从浏览器渲染页面机制的方向
我们一般说的浏览器内核可以分成2部分:JS引擎
和渲染引擎
。JS引擎就是专门解释js代码的;渲染引擎又包含了:HTML解释器、CSS解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。整体来看浏览器的一次渲染过程就是:调用浏览器各个零部件把网页资源文件从服务器上下载下来,转换为图像的过程。渲染过程对我们前端开发来说是个黑盒,但了解了其中的机制可以帮助我们避免一些问题。
再详细点的渲染过程是:
(1)HTML文件下载完以后,HTML解析器就开始工作了,它自顶向下地解析HTML元素,最终生成DOM树;
(2)当HTML解析到link标签或者style标签之后,CSS解析器就开始工作了,开始构建CSSOM树;
(3)DOM树和CSSOM树都构建完成后,两者结合生成了Render Tree——渲染树,其实就是带有样式规则的DOM树;
(4)然后布局引擎开始工作,递归地为渲染树的每个元素计算它的尺寸、位置,给出它们出现在屏幕上的精确坐标;
(5)最后图形引擎(和GPU有关)开始工作,遍历渲染树,把每个节点绘制出来。
可以看到从创建渲染树开始就是彻底的黑盒了,我们能优化的地方不多。另外可以看到,刚才我说的这个流程里是没有JS参与的,因为JS的本质是修改DOM
,没有它渲染照样是完整的,这里先不考虑,后面再说。这个流程里很重要的一点是HTML解析和CSS解析是并行的,DOM树和CSSOM树都生成完毕了才能进行下一步——构建渲染树。所以如果DOM树已经生成了,但CSSOM树还没生成,我们就说CSS阻塞了,这个时候页面是空白的,即便已经有DOM树了,只要CSSOM不ok,渲染就没法进行下去(目的是避免HTML文本裸奔在用户眼前)。所以,业内的一个共识是:把CSS放在HTML尽量靠前,尽早解析到link或style标签,让CSS解析器工作起来。
ok,我们一方面要让CSS解析器尽早工作起来,还要让它工作快起来。我们能做的就是减少CSS选择器复杂度。提升的方案主要是:
(1)后代选择器的开销是最高的,深度不要超过3层,尽可能使用id和类来关联;
(2)少用标签选择器,用类选择器替代;
(3)那些可以继承的属性,避免重复匹配重复定义;
(4)避免使用通配符,只对需要用到的元素进行选择。
另外非常重要一点的是,css选择器是从右到左解析的
,#myList li{} 像这种写法,看上去是先找到了id为myList的元素,缩短了时间,但它实际是先找到所有li标签,再找到哪个是包含在myList下的。所以这种情况直接用类选择器最合适了。
最后来说有JS参与进来的情况。JS的问题是阻塞
。JS引擎虽然独立于渲染引擎(也就是不同线程),按理说,他们也可以并行工作,但是JS代码里充满了DOM的操作、CSS的操作,所以为了保证JS执行的时候有确定的DOM和CSS,JS执行的时候DOM和CSSOM的构建都会暂停
。也就是当HTML解析到script标签时,浏览器会暂停渲染过程,将控制权交给JS引擎,JS引擎执行script标签内的代码(外部的JS还要先下载再执行),执行完了再把控制权交给渲染引擎,继续DOM和CSSOM的构建。所以这种情况你把script标签写在html最后也无济于事,还是会阻塞。浏览器之所以会一股脑的暂停DOM树构建,是因为浏览器不知道这块js代码有没有操作DOM, 但是我们开发是知道的,如果js代码没有操作DOM和CSS,那就完全可以并行执行,所以HTML也提供了async和defer属性,来避免没必要的阻塞。
默认模式(阻塞下载,下载完立刻执行)
。
async模式(异步下载,下载完立刻执行)
:<script async src="xxx.js"></script> HTML解析器解析到这儿时会异步的下载xxx.js文件,但是当下载结束时,js会立刻执行,这个时候渲染引擎也会被暂停。
defer模式(异步下载,下载完延迟执行)
:<script defer src="xxx.js"></script> 和async一样,xxx.js也是异步下载,但是它会等到DOM树和CSSOM树都构建完成后再执行。