当我们在浏览器地址栏输入网址 ,回车,回车这一瞬间到看到页面到底发生了什么呢?
域名解析 –> 发起TCP的3次握手 –> 建立TCP连接后发起http请求 –> 服务器响应http请求,浏览器得到html代码 –> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) –> 浏览器对页面进行渲染呈现给用户
规则一:减少http请求
雪碧图:CSS Sprites,若干小图标拼合成一张图后布局(background-position: -260px -90px;)
雪碧图减少了浏览器的请求次数,但有自己的问题,高清屏会失真,不方便变化。
图标字体,可解决这些问题。
@font-face {
font-family: "Bitstream Vera Serif Bold";
src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
}
- font-family:设置文本的字体名称。之后可以在定义字体的字体栈中使用这个名称。
- src:设置自定义字体的相对路径或者绝对路径。
图标字体优势
-
轻量级:一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,不需要下载一个个图像。这样可以减少HTTP的请求数量,而且和HTML5的离线存储配合,可以对性能做出优化。
-
灵活性:不调字体可以像页面中的文字一样,通过font-size属性来对其进行大小的设置,而且还可以添加各种文字效果,如color、hover、filter、text-shadow、transform等效果。灵活的简直不像话!
-
兼容性:图标字体支持现代浏览器,甚至是低版本的IE浏览器,所以可以放心的使用它。
-
相比于位图放大图片会出现失真、缩小又会浪费掉像素点,图标字体不会出现这种情况
小图片转base64,可以节省一个http请求
规则三:添加Expires头,缓存图片、样式表、脚本
<<===
Expires头和Cache-Control结合使用
Expires:Wed, 16 Oct 2024 05:42:03 GMT
Cache-Control:max-age=315360000
规则四:压缩组件
===>>Accept-Encoding: gzip, deflate
服务器压缩响应:
<<===Content-Encoding: gzip
规则五:将样式表放在顶部,使用link标签将样式表放在head中
“CSS at the Top”
样式表放如在底部会导致 无样式闪烁FOUC、浏览器行为 白屏。放在顶部,就可以避免这些问题。
规则六:将脚本放在底部,script标签放在底部
脚本放在顶部对Web页面影响:会阻塞后面内容的呈现。阻塞后面组件的下载
规则十一:避免重定向,重定向会使页面变慢
301 moved permancently
302 moved Temporatily
303 see other
304 not modified
规则十三:配置Etag
===>GET
<===Etag: '10c24bc-4ab-457e1c1f'
第二次
===>If-None-Match: '10c24bc-4ab-457e1c1f'
<===304 Not Modified
其他:
使用CDN(Content Delivery Network,内容分发网络)
webpack性能优化
参考:https://github.com/wisestcoder/blog/issues/2
1、前言
随着前端的发展,在一个前端项目中,框架和构建工具已经成了编配,而webpack显然已经成了最火热的构架工具之一。React,Vue,angularjs2等诸多知名项目也都选用其作为官方构建工具,极受业内追捧,随者工程开发的复杂程度和代码规模不断地增加,webpack暴露出来的各种性能问题也愈发明显,极大的影响着开发过程中的体验。
本文旨在分析 webpack
的性能问题,并提供不同的解决方案。
2、性能问题源自何处
- 项目体积过大,有时只是一个小改动,但热更新的全量构建导致编译时间出奇的长。
- 多个模块之间共用基础资源存在重复打包,代码复用率不高。
- 一些具有公共特性的代码没有提取成通用组件。
- 一些代码库被打包在项目中,导致项目编译时间太长;而且不利于做缓存。
- 图片等静态资源没有走cdn
- 单页面项目过大,导致首次加载时间太长
在此我们介绍一款 wepback
的可视化资源分析工具:webpack-visualizer,这款工具可以在webpack构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布情况,方便做更精确的资源依赖和引用的分析。
3、解决方案
我们主要针对不同的性能问题提供不同的解决方案。
3.1 合理去除对一些代码库的构建
从上图中我们不难发现大多数的工程项目中,依赖库的体积永远是大头,通常体积可以占据整个工程项目的7-9成,而且在每次开发过程中也会重新读取和编译对应的依赖资源,这其实是很大的的资源开销浪费,而且对编译结果影响微乎其微,毕竟在实际业务开发中,我们很少会去主动修改第三方库中的源码,所以我们需要通过一些方案来抽离代码库。
1. 使用 externals
配置来提取常用库
externals
的官方定义是:防止将某些 import
的包(package)打包到 bundle
中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
例如,从 CDN 引入 react
,而不是把它打包:
index.html
...
<script src="https://cdn.bootcss.com/react/15.6.1/react.js"></script>
...
webpack.config.js
externals: {
react: 'React'
}
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import react from 'react';
简单来说 external
就是把我们的依赖资源声明为一个外部依赖,然后通过 script
外链脚本引入。这也是我们早期页面开发中资源引入的一种翻版,只是通过配置后可以告知 webapck
遇到此类变量名时就可以不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提升编译速度,同时也能更好的利用CDN来实现缓存。
2. 利用 DllPlugin
和 DllReferencePlugin
预编译资源模块
我们的项目依赖中通常会引用大量的npm包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。
简单来说 DllPlugin
的作用是预先编译一些模块,而 DllReferencePlugin
则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin
必须要在 DllReferencePlugin
执行前先执行一次, dll
这个概念应该也是借鉴了windows程序开发中的 dll
文件的设计理念。
相较于 externals
,DllPlugin
的主要是:
- 由于
externals
的配置项需要对每个依赖库进行逐个定制,所以每次引入新的代码库的时候都需要手动修改外链的引入,并且在CDN上配置该代码库的资源,过程比较繁琐,而通过dllPlugin
则能完全通过配置读取,减少维护的成本。 DllPlugin
会将多个代码库抽离成一个js资源,可以减少一些script
标签。
(1) 配置 dllPlugin
对应资源表并编译文件
dll.config.js
const webpack = require('webpack');
const path = require('path');
const vendors = [
'react',
'react-dom',
'react-router'
];
module.exports = {
output: {
path: __dirname + '/dist',
filename: '[name].js',
library: '[name]',
},
entry: {
lib: vendors,
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dll', 'manifest.json'),
name: '[name]',
context: __dirname,
}),
],
};
然后执行命令:
NODE_ENV=development webpack --config webpack.dll.lib.js
结果会生成一个 manifest.json
文件和一个 lib.js
文件。
manifest.json
记录了webpack中的预编译信息,这样等于提前拿到了依赖库中的chunk信息,在实际开发过程中就无需要进行重复编译。lib.js
就是将配置的代码库编译后生成的文件。
(2) dllPlugin
的静态资源引入
生成了 manifest.json
文件和 lib.js
文件之后,我们还要在我们的配置文件中配置 manifest.json
,让 webpack
能够不自动编译这些代码库,配置如下:
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/manifest.json'),
})
]
注意:如果你有依赖代码库相同的项目,也可以使用同一份 manifest.json
和 lib.js
文件,只需在配置中将manifest.json
引入,在 script
标签中引入 lib.js
即可。
3.2 多入口项目合理提取出公共代码
当项目的入口很多,但是入口文件存在一些公共代码,对所有依赖的chunk进行公共部分的提取的必要性就会发挥出来。
- 默认会把所有入口节点的公共代码提取出来, 生成一个common.js
new webpack.optimize.CommonsChunkPlugin('common.js')
- 有选择的提取公共代码
new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);
- 指定模块必须被入口chunk 共享的数目
new webpack.optimize.CommonsChunkPlugin({
name: "commons",
minChunks: 3
filename: "commons.js"
})
- 抽取enry中的一些lib抽取到vendors中
entry = {
vendors: ['fetch', 'loadash']
};
new webpack.optimize.CommonsChunkPlugin({
name: "vendors",
minChunks: Infinity
});
3.3 单页面应用合理分割代码、按需加载
现在很多项目都采用单页面开发,特别是一些移动端的网站;但是当网站规模越来越大的时候,首先出现的问题是 Javascript 文件变得巨大,这导致首页渲染的时间让人难以忍受。实际上程序应当只加载当前渲染页所需的 JavaScript,也就是大家说的“代码分拆" — 将所有的代码分拆成多个小包,在用户浏览过程中按需加载。
通过代码分割,我们得到的效果如下:
分割之前的页面
分割之后的效果
可以很清楚的看到,我们将一个大的js文件拆分成了若干个chunk文件。
我们项目的结构如下:
page
├── home
│ ├── home.js
│ ├── home.scss
├── guide
│ ├── guide.js
│ ├── guide.scss
└── more
│ ├── more.js
│ └── more.scss
└── app.js
按需加载之后,我们需要对Route进行改造,我们将component方法替换成getComponent,让路由去动态的加载组件。
app.js是项目入口,配置如下:
const rootRoute = {
indexRoute: {
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./home'))
})
}
},
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./index'))
})
},
path: '/',
childRoutes: [
require('./guide'),
require('./more')
]
}
render((
<Router
history={hashHistory}
routes={rootRoute}
/>
), document.getElementById('app'))
此处有四个属性:
path
将匹配的路由,也就是以前的 path。
getComponent
对应于以前的 component 属性,但是这个方法是异步的,也就是当路由匹配时,才会调用这个方法。
这里面有个 require.ensure
方法
require.ensure(dependencies, callback, chunkName)
这是 webpack 提供的方法,这也是按需加载的核心方法。第一个参数是依赖,第二个是回调函数,第三个就是上面提到的 chunkName,用来指定这个 chunk file 的 name。
如果需要返回多个子组件,则使用 getComponents
方法,将多个组件作为一个对象的属性通过 cb 返回出去即可。这个在官方示例也有,但是我们这里并不需要,而且根组件是不能返回多个子组件的,所以使用 getComponent
。
indexRoute
indexRoute
用来显示默认路由,不需要进行按需加载。
childRoutes
这里面放置的就是子路由的配置,这里的子路由都应该是按需加载的。
我们还需要在子路由中进行配置。
home.js
module.exports = require('./home');
由于home是默认的路由,所以不需要进行按需加载
guide.js
module.exports = {
path: '/guide',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./guide'))
})
}
}
more.js
module.exports = {
path: '/more',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./more'))
})
}
}
项目经过webpack打包之后,会生成包含子路由的chunk文件,并且在路由切换的时候进行按需加载。
3.4 加快代码压缩速度
UglifyJsPlugin
凭借基于node开发,压缩比例高,使用方便等诸多优点已经成为了js压缩工具中的首选,但是我们在webpack的构建中观察发现,当webpack build进度走到80%前后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是 UglifyJsPlugin
在对我们的 output
中的 bunlde
部分进行压缩耗时过长导致,针对这块我们推荐使用webpack-uglify-parallel来提升压缩速度。
webpack-uglify-parallel
的实现原理是采用了多核并行压缩的方式来提升我们的压缩速度。
使用配置也非常简单,只需要将我们原来webpack中自带的 UglifyJsPlugin
配置:
new webpack.optimize.UglifyJsPlugin({
exclude:/\.min\.js$/
mangle:true,
compress: { warnings: false },
output: { comments: false }
})
修改成如下代码即可:
const os = require('os');
const UglifyJsParallelPlugin = require('webpack-uglify-parallel');
new UglifyJsParallelPlugin({
workers: os.cpus().length,
mangle: true,
compressor: {
warnings: false,
drop_console: true,
drop_debugger: true
}
})
3.5 让loader多进程地去处理文件
happypack 的原理是让loader可以多进程去处理文件,原理如图示:
此外,happypack同时还利用缓存来使得rebuild 更快
var HappyPack = require('happypack'),
os = require('os'),
happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
modules: {
loaders: [
{
test: /\.js|jsx$/,
loader: 'HappyPack/loader?id=jsHappy',
exclude: /node_modules/
}
]
}
plugins: [
new HappyPack({
id: 'jsHappy',
cache: true,
threadPool: happyThreadPool,
loaders: [{
path: 'babel',
query: {
cacheDirectory: '.webpack_cache',
presets: [
'es2015',
'react'
]
}
}]
}),
//如果有单独提取css文件的话
new HappyPack({
id: 'lessHappy',
loaders: ['style','css','less']
})
]
4、结尾
性能优化无小事,追求快没有止境,在前端工程日益庞大复杂的今天,针对实际项目,持续改进构建工具的性能,对项目开发效率的提升和工具深度理解都是极其有益的。
5、参考文章