前端性能优化思路总结

1,278

概述

找到目标逐个击破

明确优化方向

从一个面试题引出前端优化需要注意的几个方向:

从用户输入URL到页面加载完成,这中间都发生了what

我希望各位读者可以在心中思考一下这一问题,在这一过程中的每个阶段都可在性能优化中做文章。 现在我们简单的复习一下此过程: 首先,需要通过DNS(域名解析系统)将URL解析为对应的IP地址,然后与此IP地址相关的服务器建立TCP网络连接,之后,客户端向服务端发起HTTP请求(request),服务端接收请求并处理后,将结果数据放在HTTP响应中(response)返回给客户端,获取服务端的响应数据后,浏览器对数据进行渲染,渲染完毕,页面展示在浏览器中,并时刻响应用户的进一步操作。

将这一流程切分为如下阶段进行逐一分析:

  1. DNS解析
  2. TCP连接
  3. HTTP请求request
  4. 服务端处理请求,HTTP响应返回数据
  5. 浏览器获得响应数据并解析,将结果展示给用户

大家可以看到,每次URL改变时,都需执行以上的五个阶段,因此在考虑性能优化方案时,我们都需要从以上五个阶段来考虑。

实践检验理论

接下来的任务,就是针对这五个阶段,进行细化、分解,逐个提出问题,逐个击破

具体工作分解

  • DNS解析花费时间,可以减少解析次数或将解析前置吗?能
    1. 浏览器DNS缓存
    2. DNS prefetch
  • TCP的三次握手有无解决方案?有
    1. 长链接
    2. 预链接
    3. 接入SPDY协议
  • 前端的HTTP请求
    1. 减少请求次数
    2. 减少请求体积
  • 在部署时将静态资源放在离我们较近的CDN上

以上提到的四点都是基于网络层面的优化,下面是基于浏览器方面的优化

  • 资源加载优化
  • 服务端渲染
  • 浏览器缓存机制
  • DOM树的构建
  • 页面排版和渲染过程
  • 回流与重绘的权衡
  • 合理的DOM操作

网络层面的优化

在上一章中分析的五个阶段,主要涉及到网络层面的优化,主要有以下三个阶段

  • DNS解析
  • TCP连接
  • HTTP请求/响应

前两个阶段主要与服务端相关,因此,我们主要关注HTTP连接这一方面的优化,主要分为两大方向:

  • 减少请求次数
  • 减少单次的请求体积

以上两点主要对应开发中常见的操作: 资源压缩与合并。现今主要是通过构建工具完成此方面的工作。因此,主要讲一下webpack的使用

webpack的构建瓶颈

对于每个用过webpack的读者都使用过‘打包’和‘压缩’,此处主要将注意力放在webpack的性能优化上

webpack的优化瓶颈主要分为以下两个方面:

  • 构建过程耗时长
  • 打包结果体积大

webpack优化方案

构建结果分析

项目完成之后,建议开发人员先打一个版,看一下效果,然后根据结果去做优化,做到有的放矢

文件结构可视化-webpack-bundle-analyzer

在打包之前先安装此可视化工具,它会以矩形树图的形式将包内各个模块的大小和依赖关系呈现出来,便于分析优化

// 安装
npm i --save-dev webpack-bundle-analyzer

在使用时,将其以插件的形式引入

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
    plugins: [
        new BundleAnalyzerPlugin()
    ]
}

构建过程优化

优化loader配置

loader处理文件的转换操作是很耗时的,所以需要让尽可能少的文件被loader处理

以babel-loader为例,使用include/exclude来避免不必要的转译,并开启缓存提升babel-loader的工作效率

  • 在include/exclude中使用绝对路径,更倾向于使用include
  • 在babel-loader中开启缓存 ?cacheDirectory=true
const path = require('path')
// ...
module: {
    rules: [
        {
            test: /\.js[x]$/,
            // 
            include: [path.resolve(__dirname, 'src')],
            use: {
                loader: 'babel-loader?cacheDirectory=true',
                options: {
                    presets: ['@babel/preset-env']
                }
            }
        }
    ]
}
特殊照顾第三方库

第三方库一般存放于node_module中,对于它们的理想处理方式是,只在第一次打包时对其进行处理,以后只要当前依赖包的版本未发生变化就无须重新打包,推荐大家使用DllPlugin

使用DllPlugin处理文件,主要分为以下两步:

  • 基于dll专属的配置文件webpack.config.dll.js,打包dll库
  • 基于webpack.config.js文件,打包业务代码

dll配置文件编写如下:

const path = require('path')
const webpack = require('webpack')
module.exports = {
    entry: {
        // 依赖包数组
        vender: [
            'prop-types',
            'babel-polyfill',
            'react',
            'react-dom',
            'react-router-dom'
        ]
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js',
        library: '[name]_[hash]'
    },
    plugins: [
        new webpack.DllPlugin({
            // DllPlugin的name属性要和library保持一致
            name: '[name]_[hash]',
            path: path.join(__dirname, 'dist', '[name]-manifest.json'),
            // context需要和webpack.config.js保持一致
            context: __dirname
        })
    ]
}

之后在webpack.config.js中针对dll稍作配置:

const path = require('path')
const webpack = require('webpack')

module.exports = {
    mode: 'production',
    // 编译入口
    entry: {
        main: './src/index.js'
    },
    // 目标文件
    output: {
        path: path.join(__dirname, 'dist/'),
        filename: '[name].js'
    },
    // dll相关配置
    plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            // manifest就是我们第一步中打包出来的json文件
            manifest: require('./dist/vendor-manifest.json')
        })
    ]
}
减少一核工作多核围观的情况-Happypack

因webpack是单线程,即使此刻存在多个任务等待处理,也需要排队一个一个来,这是单线程的缺点,好在CPU是多核的,使用Happypack会充分利用CPU的多核优势,将多个任务分解为多个子进程去并发执行,提高打包效率

注: 当项目比较小时不建议使用Happypack进行配置

Happypack的使用很简单,只需将对应的loader配置转移到Happypack中就可以,在此之前需要告知Happypack我们需要多少并发的进程:

// 先安装happypack
npm i -D happypack
const Happypack = require('happypack')
// 手动创建进程池
const happyThreadPool = Happypack.ThreadPool({size: os.cpus().length})

module.exports = {
    module: {
        rules: [
            ...
            {
                text: /\.js$/,
                // ?后面的查询参数指定了处理这类文件的Happypack实例的名字,唯一
                loader: 'happypack/loader?id=happyBabel',
                ...
            }
        ],
    },
    plugins: [
            ...
            new Happypack({
                // 此处id的名字和上面的查询参数必须相对应
                id: 'happyBabel',
                // 指定线程池
                threadPool: happyThreadPool,
                loaders: ['babel-loader?cacheDirectory']
            })
        ]
}

图片优化

当前比较流行的图片格式有JPEG/JPG、PNG、Base64、SVG、Webp等,还有精灵图(CSS Sprites)至今仍还有使用,下面主要介绍一下其特性和应用场景

JPEG/JPG

特性: 有损压缩、体积小、不支持透明

优点
  • 通用,大多数浏览器都支持的图片格式
  • 压缩算法高效,图片格式体积小
  • 以24位存储单个图可呈现1600多万中颜色,色彩丰富
不足
  • 图片颜色对比强烈,压缩后导致图片模糊
  • 不支持透明度
应用场景

JPG适用于呈现色彩丰富的图片,即使是大图,其体积也比较小

  • 页面背景图
  • 轮播图
  • 电商中商品展示图

PNG 8 / PNG 24

特性: 支持透明、无损压缩、体积大

优点
  • 支持透明
  • 无损压缩的高保真图片格式
  • PNG 8最多支持256种颜色
  • PNG 24最多支持1600万中颜色
  • 色彩表现力更强,对线条轮廓处理更细腻
不足
  • 体积大

在项目中,不推荐使用PNG处理颜色复杂的图片,优先选用PNG8格式

应用场景
  • 一些小图-logo
  • 颜色简单,对比明显的图片
  • 支持透明

Base64

将一张图片数据编码成一串字符串,使用该字符串代替图像地址url

特性: 减少http请求、需要编码、小图标使用

优点
  • 将图片进行Base64编码,可以直接将结果写入html/css
  • 减少http请求
不足
  • 增加html/css文件体积
  • 解析时间增加
  • Base64 编码后,图片大小会膨胀30%左右
应用场景
  • 图片实际尺寸很小(<2kb)
  • 图片无法以雪碧图的形式与其它小图结合
  • 图片更新频率低

SVG

特性: 体积小、不失真、兼容好

可缩放矢量图形(Scalable Vector Graphics),一种 XML 应用,以一种简洁、可移植的形式表示图形信息

优点
  • 图片可无限放大缩小不失真
  • 文件体积更小
  • 压缩性强
不足
  • 浏览器的渲染成本高
  • 与其它格式的图片相比,学习成本高
应用
  • 直接放在html中,成为DOM的一部分
    <body>
        <svg xmlns="http://www.w3.org/2000/svg"   width="100" height="100">
            <circle cx="40" cy=45" r="50" />
        </svg>
    </body>
    
  • 以 .svg格式存储,引入html中
    <img src="./xx/文件名.svg" />
    

WebP

特性: 集多家优点于一身,全能选手

是Google专为Web开发的一种加快图片加载速度的图片格式

优点
  • 支持透明
  • 可以显示动图
  • 显示图形丰富
不足
  • 增加服务器的负担
  • 兼容问题

待续