不要单点关注,建议用系统化思维,从全局理解,举个例子:
Web端常规的生命周期分为三部分,且每个生命周期阶段,可执行的优化思路,是不一样的
一、资源加载:少、小、无
二、程序渲染:加快、不重复
三、运行中:内存、CPU
有了上面的阶段和思路,在去思考具体的执行方法,这样不会有遗漏
万变不离其宗,自认为上面的就是宗,业内所有『花活』都脱不开上面的三大类
在贴一个脑图,很久之前整理的:
http://naotu.baidu.com/file/4cde3cbd2efde477fd5a86a515b4ab57?token=535fadce9dba7a7d
浏览器发送一个完整的请求需要经过:DNS寻址、与服务器进行连接、发送数据、等待服务器响应、接收数据。
1、页面级别的优化
(1) 减少HTTP请求
(2) 减少DNS查询:DNS缓存、将资源分布到恰当数量的主机名
(3) 将外部脚本置底
外链脚本在加载时,会阻塞其他资源。 最简单可依赖的方法是将脚本尽可能往后挪,减少对并发下载的影响。
(4) 将CSS放在head中
(5) 异步执行inline脚本。可使用script的defer属性,使用setTimeout,HTML5中的web workers机制等
(6) 合理设置HTTP缓存:Expires 与Cache-control
2、代码级别的优化
(1) javascript
(2) CSS
3、前端性能优化最佳实践
无cookie域名:发送一个请求时,同时还要请求一张静态的图片和发送cookie,服务器对于这些cookie不会做任何使用。
在之前的文章中介绍过前端预渲染方法提升页面性能,并介绍了预渲染的原理和实践
灵题库:lingtiku网站首屏速度优化实践:前端预渲染但是在使用前端路由的单页应用中,还是会有些问题,本文介绍另一种页面优化方案:数据预下载,并写一个rollup插件来实现数据预下载的功能。
我们知道前端预渲染的原理是:构建打包之后,插件会在本地启动express静态服务,serve打包好的静态资源。然后再启动一个无头浏览器(例如Puppeteer
),浏览器从服务器请求网页,网页运行时候会请求首屏接口,用拿到的数据渲染出包含内容的首屏后,无头浏览器截屏并替换掉原来的html。
但是在单页应用中存在一个问题,前端路由切换时候,还是会请求接口,而不会加载预渲染好的html,例如预渲染index.html和about.html,但是从index.html跳转about.html时候,如果是前端路由,不会从服务器请求about.html,而是加载about组件,那还是会请求接口,从而可能带来界面渲染慢的情况。
怎么解决这个问题呢?
在优化灵题库(https://www.lingtiku.com)页面加载时候,我想到一个简单的办法,就是仿照前端预渲染的思路,在构建阶段就把一些数据预下载,下载之后保存在一个json文件里,json文件放到前端静态资源目录中。然后前端发起请求时候,用axios拦截预下载的请求,改成请求静态的json数据。
这样会带来两个好处
写一个rollup插件,来实现数据预下载的功能,这个插件需要做以下事情
在rollup插件中,需要实现transform钩子,来将配置的json文件名和接口的映射注入到代码中;还要实现写bundle构造,在这个阶段请求数据并把json文件输出到构建目录中。
插件使用request
库来请求数据。
另外需要注意,为了保证数据改变时候可以及时更新,需要给json加一个版本号,这里的版本号简单地设置成当前时间,其实使用内容的md5更合适一些。
插件代码如下:
// preload-data-plugin.js
import fs from 'fs';
import path from 'path';
import request from 'request';
import{promisify}from 'util';
import crypto from 'crypto';
const writeFileAsync=promisify(fs.writeFile);
const version=String(Date.now());
const preloadData=options=>{
const{map, staticDir}=options;
Object.entries(map).forEach(([name, urlOrParams])=>{
request(urlOrParams, (err, res, body)=>{
writeFileAsync(path.join(staticDir, `${name}_${version}.json`), body);
});
});
};
const preloadDataPlugin=(options={})=> ({
name: 'preloadDataPlugin',
transform: (code, id)=>{
const map={};
Object.entries(options.map).forEach(([key, value])=>{
map[`${key}_${version}`]=value;
});
return{
code: code.replace(/__PRELOAD_DATA_MAP__/g, JSON.stringify(map))
}
},
writeBundle: ()=>{
try{
preloadData(options)
}
catch (e){
console.error(e)
}
}
});
export default preloadDataPlugin;
rollup的插件配置如下
// rollup.config.js
import preloadDataPlugin from 'https://www.zhihu.com/question/rollup-plugin/preload-data-plugin';
export default{
// ...other config
plugins:[
preloadDataPlugin({
staticDir: path.resolve(__dirname, 'dist'),
map:{
'main': 'https://api.example.com/all',
'quiz_1': 'https://api.example.com/quiz/find?id=1',
'quiz_2': 'https://api.example.com/quiz/find?id=2',
'quiz_3': 'https://api.example.com/quiz/find?id=3',
}
}),
]
};
最终会在dist目录生成几个文件
├── main_1651497197552.json
├── quiz_1_1651497197552.json
├── quiz_2_1651497197552.json
└── quiz_3_1651497197552.json
axios拦截代请求,替换成请求的json码如下
// http.js
import axios from 'axios';
const parseURL=url=>
url.split('?')[1].split('&').filter(Boolean)
.reduce((result, paramPair)=>{
const[key, value]=paramPair.split('=');
result[key]=value;
return result;
},{});
axios.interceptors.request.use(
function (config){
Object.entries(__PRELOAD_DATA_MAP__).some(([name, url])=>{
const query=parseURL(url);
const isParamsEqual=!config.params || Object.entries(query).every(([key, value])=>{
return config.params[key]==value;
});
// 接口和参数都匹配,则替换
if (url.includes(`${config.baseURL || ''}${config.url}`) && isParamsEqual){
config.baseURL='';
config.url=`/${name}.json`;
config.params={};
}
});
return config;
},
function (error){
return Promise.reject(error);
}
);
再说另一个问题:如果数据很大,及时很快请求到,但是长列表渲染也会很慢。
目前灵题库的解决方法是通过懒加载方式解决,先只渲染前20条。后面的数据渲染过程中加loading,以提供好的交互体验。当然这种问题通过分页来解决是最优雅的方案。