400-123-4567

web 前端有哪些性能优化?发布日期:2024-02-28 00:22:11 浏览次数:

不要单点关注,建议用系统化思维,从全局理解,举个例子:

Web端常规的生命周期分为三部分,且每个生命周期阶段,可执行的优化思路,是不一样的

一、资源加载:少、小、无

二、程序渲染:加快、不重复

三、运行中:内存、CPU

有了上面的阶段和思路,在去思考具体的执行方法,这样不会有遗漏

万变不离其宗,自认为上面的就是宗,业内所有『花活』都脱不开上面的三大类

在贴一个脑图,很久之前整理的:

naotu.baidu.com/file/4c

浏览器发送一个完整的请求需要经过:DNS寻址、与服务器进行连接、发送数据、等待服务器响应、接收数据。

1、页面级别的优化

(1) 减少HTTP请求

  • CSS精灵图
  • 如果可以,尽量将外部的脚本、样式进行合并,多个合为一个。且CSS,JS,image都可以用相应工具进行压缩
  • 能使用CSS替代效果的尽量少使用图片
  • 图片懒加载。可以在某些条件下或者页面刚加载时减少HTTP请求数。
  • CDN托管。

(2) 减少DNS查询:DNS缓存、将资源分布到恰当数量的主机名

(3) 将外部脚本置底

外链脚本在加载时,会阻塞其他资源。 最简单可依赖的方法是将脚本尽可能往后挪,减少对并发下载的影响。

(4) 将CSS放在head中

(5) 异步执行inline脚本。可使用script的defer属性,使用setTimeout,HTML5中的web workers机制等

(6) 合理设置HTTP缓存:Expires 与Cache-control

2、代码级别的优化

(1) javascript

  • 用innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能
  • 慎用with。with相当于增加了作用域链的长度,过长的作用域链会导致查找性能下降
  • 减少作用域链查找。若在循环中需要访问非本作用域下的变量,在遍历之前用局部变量缓存该变量,并在遍历结束之后再重复那个变量。全局变量处于作用域链的最顶端,访问时查找次数是最多的,所以少用全局变量。同时也要注意减少闭包的使用。
  • 数据访问。js中对直接量(字符串,正则表达式)和局部变量的访问最快。所以当:a.对任何对象属性访问超过1次;b. 对任何数组成员访问次数超过1次。可以将数据放入局部变量以加快访问速度。

(2) CSS

  • 使用link而不使用@import。link在页面加载时CSS同时被加载,@import引入的CSS要等页面加载完毕后再加载
  • 设置className而不是直接使用css表达式
  • 避免正则的属性选择器。

3、前端性能优化最佳实践

  • 性能评级工具(PageSpeed 或 YSlow)
  • 合理设置 HTTP 缓存:Expires 与 Cache-control
  • 静态资源打包,开启 Gzip 压缩(节省响应流量)
  • CSS3 模拟图像,图标base64(降低请求数)
  • 模块延迟(defer)加载/异步(async)加载
  • Cookie 隔离(节省请求流量)
  • localStorage(本地存储)
  • 使用 CDN 加速(访问最近服务器)
  • 启用 HTTP/2(多路复用,并行加载)
  • 前端自动化(gulp/webpack)

推荐一个技术导航网站 it1211.com
  • 减少 HTTP 请求数(通过文件合并、css 雪碧图、图片的延迟加载,也叫做赖加载、合并压缩css样式表和js脚本、使用 base64、 充分利用缓存。等方式来),避免过多的请求造成等待的情况。
  • 通过 DNS 缓存等机制来减少 DNS 的查询次数。
  • 通过设置缓存策略,对不变化的资源进行缓存。
  • 通过延迟加载的方式,来减少页面首屏加载时需要请求的资源,等用户需要访问时候,再去请求加载。
  • 通过用户行为,对某些资源使用预加载的方式,提高响应速度。
  • 6. 样式表和JS文件的优化,以及代码结构的优化,将css样式表文件放到文件的头部,让CSS样式表尽早地完成下载。对应js脚本文件,一般我们把他放到页面的尾部</body>前面。
  • 尽量使用CDN 服务,来提高用户对于资源请求时的响应速度。
  • 服务器端自用 Gzip(web服务配置文件中设置一下即可。以Apache为例,在配置文件httpd.conf中添加)、Deflate 等方式对于传输的资源进行压缩,减少传输文件的体积大小。
  • 尽量减小 cookie 的大小,并且通过将静态资源分配到其他域名下,来避免对静态资源请求时携带不必要的 cookie;

无cookie域名:发送一个请求时,同时还要请求一张静态的图片和发送cookie,服务器对于这些cookie不会做任何使用。

在之前的文章中介绍过前端预渲染方法提升页面性能,并介绍了预渲染的原理和实践

灵题库:lingtiku网站首屏速度优化实践:前端预渲染

但是在使用前端路由的单页应用中,还是会有些问题,本文介绍另一种页面优化方案:数据预下载,并写一个rollup插件来实现数据预下载的功能。

我们知道前端预渲染的原理是:构建打包之后,插件会在本地启动express静态服务,serve打包好的静态资源。然后再启动一个无头浏览器(例如Puppeteer),浏览器从服务器请求网页,网页运行时候会请求首屏接口,用拿到的数据渲染出包含内容的首屏后,无头浏览器截屏并替换掉原来的html。

但是在单页应用中存在一个问题,前端路由切换时候,还是会请求接口,而不会加载预渲染好的html,例如预渲染index.html和about.html,但是从index.html跳转about.html时候,如果是前端路由,不会从服务器请求about.html,而是加载about组件,那还是会请求接口,从而可能带来界面渲染慢的情况。

怎么解决这个问题呢?

在优化灵题库(lingtiku.com)页面加载时候,我想到一个简单的办法,就是仿照前端预渲染的思路,在构建阶段就把一些数据预下载,下载之后保存在一个json文件里,json文件放到前端静态资源目录中。然后前端发起请求时候,用axios拦截预下载的请求,改成请求静态的json数据。

这样会带来两个好处

  • 减小服务器压力,不需要读数据库和缓存
  • 对于json数据,初始读取一次,后续都可以读浏览器的缓存,速度非常快。而且初始请求因为是静态资源,速度也会比接口更快一些。


写一个rollup插件,来实现数据预下载的功能,这个插件需要做以下事情

  1. 根据插件配置,获取所有的需要预下载的数据接口和接口下载完成保存成的文件名字。还有打包构建输出的静态资源目录。
  2. 在打包输出文件阶段,请求接口并将数据保存到指定文件名的json文件,并将文件放到静态资源目录。
  3. 将请求接口和json文件名的映射关系注入到代码中,以便axios可以拦截处理。


在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,以提供好的交互体验。当然这种问题通过分页来解决是最优雅的方案。

  1. 目前只是简单地支持get请求,可以考虑扩展一下,支持post请求。
  2. 安全方面,为了避免数据被轻易爬取,可以考虑让插件支持将请求数据base64加密,然后前端解密,这样增加了爬数据的成本。

【尚学堂】前端Web开发_Axios网络请求封装;


平台注册入口