项目生产环境前端静态资源总体积过大,图片资源未压缩,请求接口 30+ 个,整体平均加载耗时 20S,用户体验比较差劲。
减少静态资源的体积,接口数量和加载的时长:
一般来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。
DLL(Dynamic Link Library)文件为动态链接库文件,在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。
使用DLLPlugin
打包需要分离到动态库的模块,DllPlugin
是webpack
内置的插件,不需要额外安装,直接配置webpack.dll.config.js
文件:
module.exports = {=
entry: {
vue: ['vue', 'vue-router', 'vuex']
},
output: {
filename: '[name].dll.js',
path: resolve('dist/dll'),
library: '[name]_dll_[hash]'
},
plugins: [
// 接入 DllPlugin
new webpack.DllPlugin({
name: '[name]_dll_[hash]',
path: path.join(__dirname, 'dist/dll', '[name].manifest.json')
}),
]
}
在主构建配置文件使用动态库文件,在webpack.config
中使用dll要用到DllReferencePlugin
,这个插件通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上:
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dist/dll/vue.manifest.json')
}),
在入口文件引入dll文件,生成的dll暴露出的是全局函数,因此还需要在入口文件里面引入对应的dll文件。
打包构建过程中通过 HtmlWebpckPlugin 动态插入公用组件 CDN 链接,减少打包体积,加快渲染速度。
html-webpack-plugin
是 webpack 的一个插件,可以动态的创建和编辑 html 内容,在 html 中使用 esj语法
可以读取到配置中的参数,简化了html文件的构建。
webpack 项目中,所引入的第三方资源会被统一打包进 vender 文件中,我们通过 webpack 的 externals
属性可以设置打包时排除该模块。
在 webpack.config.js
中配置忽略打包资源:
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
}
创建 CDN 资源配置文件 cdn.config.js
:
export default {
js: [
'https://cdn.jsdelivr.net/npm/[email protected]',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.min.js',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js'
],
css: []
}
根据构建环境 NODE_ENV
将 CDN 地址传递给 HtmlWebpackPlugin
:
if (process.env.NODE_ENV === 'production') {
htmlWebpackConfig.cdns = CDNConfig;
}
new HtmlWebpackPlugin(htmlWebpackConfig)
重写 index.html
模版文件:
<%= htmlWebpackPlugin.options.title %>
<% for (var i in htmlWebpackPlugin.options.cdns && htmlWebpackPlugin.options.cdns.css) { %>
<% } %>
<% for (var i in htmlWebpackPlugin.options.cdns && htmlWebpackPlugin.options.cdns.js) { %>
<% } %>
是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。
是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。
结合上一步优化,在 index.html
中动态指定预加载文件:
<%= htmlWebpackPlugin.options.title %>
<% for (var i in htmlWebpackPlugin.options.cdns && htmlWebpackPlugin.options.cdns.css) { %>
<% } %>
<% for (var i in htmlWebpackPlugin.options.cdns && htmlWebpackPlugin.options.cdns.js) { %>
<% } %>
在浏览器中输入一个域名,回车后,DNS(域名系统)会先将域名解析成对应的IP地址,然后根据IP地址去找到相应的网址。这样就完成了一个DNS查找,这个查找过程当然是要消耗时间的,大约消耗20毫秒,在这个查找过程中,我们的浏览器什么都不会做,保持一片空白。如果这样的查找很多,那么我们的网页性能将会受到很大影响,这就需要用到DNS缓存和预解析。
dns-prefetch
是尝试在请求资源之前解析域名。这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。将需要预解析的 DNS 通过 link
标签进行设置:
图片延迟加载,在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片。首先可以将图片这样设置,在页面不可见时图片不会加载:
等页面可见时,使用 JS 加载图片:
const img = document.querySelector('img')
img.src = img.dataset.src
响应式图片,浏览器能够根据屏幕大小自动加载合适的图片。通过 picture
实现:
降低图片的质量,通过 image-webpack-loader
进行压缩:
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
使用 webp 格式的图片,WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。
在 Webpack 项目中可以使用 webp-loader
对本地图片进行转换:
loaders: [
{
test: /\.(jpe?g|png)$/i,
loaders: [
'file-loader',
'webp-loader?{quality: 13}'
]
}
]
使用字体图标 iconfont 代替图片图标,字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。
避免重复传输相同的数据,节省网络带宽,加速资源获取。方法是添加Expire/Cache-Control头。通过使用Expires或者Cache-Control就可以使请求的内容具有缓存性,它避免了接下来的页面访问中不必要的HTTP请求。Expires头的内容是一个时间值,值就是资源在本地的过期时间。在当前时间还没有超过缓存资源的过期时间时,就直接使用这一缓存的资源,不会发送HTTP请求。Cache-Control的作用也是类似的,只不过它的值是一个表示距离缓存过期的一个秒数时间。
Nginx 配置如下:
server {
add_header Cache-Control private;
location ~ .*\.(js|css)$ {
expires: 10d;
}
}
通过减小HTTP响应的大小也可以节省HTTP响应时间。Gzip可以压缩所有可能的文件类型,是减少文件体积、增加用户体验的简单方法。从HTTP/1.1开始,web客户端都默认支持HTTP请求中有Accept-Encoding文件头的压缩格式:Accept-Encoding: gzip
。如果web服务器在请求的文件头中检测到上面的代码,就会以客户端列出的方式压缩响应内容。Web服务器把压缩方式通过响应文件头中的Content-Encoding来返回给浏览器。
server {
add_header Cache-Control private;
location ~ .*\.(js|css)$ {
gzip on;
gzip_http_version 1.1;
gzip_comp_level 3;
gzip_types text/plain application/json application/x-javascript application/css text/javascript;
}
}
一个交互需要请求多个并行或串行接口实属正常,前端使用3g/4g等弱网络也着实是不可抗因素,所以最好的办法就是通过接口合并的方式来提高接口访问速度。
为真实的组件做一个在尺寸、样式上非常接近真实组件的组件。提升用户感知体验,保证切换一致性。在真实组件开始渲染的时候,需要一定的时间和空间,时间指的是真实组件从创建到渲染的时间,包括请求接口、请求资源和渲染的时间,空间指的是页面布局中需要给真实组件留出刚好的位置,避免产生抖动。
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。vue-router 配置路由,可以实现按需加载。
{
path: '/ceshi',
name: 'ceshi',
component: resolve => require(['../components/ceshi'], resolve)
}
使用 requestAnimationFrame 来实现视觉变化,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 requestAnimationFrame。 如果采取 setTimeout 或 setInterval 来实现动画的话,回调函数将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。
/**
* If run as a requestAnimationFrame callback, this
* will be run at the start of the frame.
*/
function updateScreen(time) {
// Make visual updates here.
}
requestAnimationFrame(updateScreen);
日常开发过程中,滚动事件做复杂计算频繁调用回调函数很可能会造成页面的卡顿,这时候我们更希望把多次计算合并成一次,只操作一个精确点,JS把这种方式称为debounce(防抖)和throttle(节流)。推荐使用 lodash
库:
// 避免窗口在变动时出现昂贵的计算开销。
$(window).on('resize', _.debounce(calculateLayout, 300));
// 避免在滚动时过分的更新定位
$(window).on('scroll', _.throttle(updatePosition, 100));
优化项目 | 优化前 | 优化后 | 备注 |
---|---|---|---|
静态文件总个数 | 150 | 10 | 减少 93.3% |
静态文件总体积 | 12.4 MB | 1.50 MB | 减少 87.8% |
模拟 Fast 3G 平均加载速度 | 8.8s | 首次加载:3.8s,二次加载1.64s | 减少 63.2% |
图片总体积 | 140K | 90K | 减少 35.7% |