前端性能优化:6.项目构建优化

通过对页面生命周期过程的了解,我们知道为了展示出想要的页面,需要许多相关的资源文件,诸如HTML文件、CSS文件、JavaScript文件及图片等其他资源文件。只有所需的资源都被浏览器请求到后,通过渲染阶段才会达到期待的页面效果,那么如何更快速地请求到资源就成为一个值得关注的优化点。

比如能否压缩请求资源的大小?能否将请求的资源进行合并以减少发起HTTP请求的数量?本文将首先分别介绍HTML文件、CSS文件、JavaScript文件压缩的原理和方法,然后从工程实践角度出发,讨论时下十分热门的前端构建工具webpack涉及的优化点,最后拓展gzip压缩对于性能优化的作用和意义。

6.1 压缩与合并

资源合并与压缩涉及的优化点包括两方面:一方面是减少HTTP请求数量,另一方面是减少HTTP请求资源的大小。

6.1.1 HTML压缩

  1. 什么是HTML压缩
    HTML是一种超文本标记语言,HTML网页本身是一种文本文件,我们在编辑文本文件的时候,是可以添加诸如空格、回车等格式化字符的,这些字符对写代码来说是非常有意义的,它们能使结构清晰,增加代码可读性。
    但是浏览器真正解析HTML的时候,这些字符是没有意义的,因为浏览器并不要求程序有很好的文件结构和代码风格。因此HTML压缩就是要删除在文本文件中有意义的,但在HTML中并不参与解析的字符。这些字符包括空格、制表符、换行符及一些其他意义的字符,如HTML注释等。
  2. 压缩效果
    如果单纯只看一个文件的压缩,可能效果并不明显,假设原始字符长度为10000,经过压缩之后的长度为9950,长度减少了0.5%。
    这能说明HTML压缩的效果不明显吗?其实不是,对大型的互联网公司来说,每一个请求的减少都是一个非常大的优化。以谷歌为例,它的网络流量占到了全网流量的40%左右,加入网络流量能达到5ZB(1ZB=10^9TB),以谷歌的流量占比,它当年的实际网络流量就是5ZB*40%=2ZB,那么当谷歌经过优化让每1MB的请求减少一个字节,则整年便可节省2000TB的流量。若以每GB流量一毛钱计算,那么一年剩下来的开支也不是个小数目。
  3. 如何压缩
    常见的压缩方式有三种:
    第一种是使用一些在线网站提供的HTML压缩服务,最基础的方式,但实际上基本不会使用这种方式。
    第二种是使用nodejs所提供的html-minifier工具进行压缩,它涉及很多参数的配置,包括是否去掉注释,是否去掉空格,是否压缩HTML中的JavaScript的minifyJS及是否压缩HTML中的CSS的minifyCSS。这种方式的可扩展性相对来说比较好。
    第三种是服务器端模板引擎的渲染压缩,这指的就是使用nodejs作为服务器端语言,模板引擎使用的是ejs等,实际上这样可以在模板引擎的渲染方法,比如express的render中,将得到的HTML文件通过调用html-minifier进行压缩。

6.1.2 CSS压缩
CSS代码也能进行压缩,而且很有必要进行压缩。

CSS压缩首先是先去掉回车和换行;然后是无效代码的删除,对有些CSS来说,无效的代码可能是注释和无效字符,需要将这些无效的代码删除,这很重要;再是CSS语义合并,通常我们写的CSS可能由于文件层级的嵌套,很难避免一定的语义重复,所以就需要进行语义合并。

对于CSS压缩的方法其实与HTML类似,有一些在线网站提供了CSS压缩的服务,可以手动进行单文件的压缩;也可以使用html-minifier针对HTML中的CSS进行压缩。

6.1.3 JavaScript压缩与混淆
JavaScript部分的处理主要包括三个方面:无效字符和注释的删除、代码语义缩减和优化及代码混淆保护。无效字符和注释的删除原理与HTML和CSS的压缩类似。这里主要介绍代码语义缩减和优化及代码混淆保护。

  1. 代码语义缩减和优化
    通过对JavaScript的压缩可以将一些变量的长度进行缩短,比如将一个原本很长的变量名用一个很短的像a、b来代替,这样能进一步有效地缩减JavaScript地代码量。同样还可以针对一些重复代码进行优化,比如去除重复的变量赋值,将一些无效的代码进行缩减与合并的优化。
let a = 1;
a = 2;
// 未对本次赋值的a进行任何使用,又进行了多余的赋值操作
// 经过优化后,仅保留 let a = 2;的赋值
let a = 2;
  1. 代码混淆保护
    由于任何能够访问到网站页面的用户,都可以通过浏览器的开发者工具查看到前端的JavaScript代码,如果前端代码的语义非常明显,没进行压缩也没进行混淆,其格式还完整保留,那么理论上任何访问网站的人都可以轻易地窥探到我们代码中的逻辑,从而产生一定安全威胁。所以进行JavaScript代码压缩和混淆的处理也是对前端代码的一种保护。
    例如,电商网站都有一套下单流程,这个流程的大部分工作都是在前端JavaScript中处理的,那么对想要窥探网站漏洞的人来说,他们一定会去尝试分析网站源码,以了解前端所做的有关下单流程的控制,从而模拟出整个下单的流程,就有可能发现在下单过程中存在的漏洞。想要预防这种行为,就需要进行JavaScript压缩,将前端原代码的可读性变得尽可能低,混淆变量与方法的命名。
  2. 如何压缩
    与HTML压缩和CSS压缩类似,JavaScript压缩处理也有类似的第三方库可供使用:uglifyJS2。可以通过npm引入uglify-js库来使用。

6.1.4 文件合并
假设我们有三个JavaScript文件,分别是a.js、b.js和c.js,当使用keep-alive模式未进行合并请求时,它的请求过程如图:
在这里插入图片描述

建立连接后,总共需要分三次请求去获取三份JavaScript文件的数据。如果是和并请求则只需要发出一个获取a-b-c.js的请求就可以接收到全部内容。
在这里插入图片描述

  1. 文件合并的优劣势
    不和并请求相比和并请求来说会有以下缺点。首先是文件与文件之间有插入的上行请求,这就增加了n-1个网络延迟,n是总共要请求的文件数量;其次是收到网络丢包的影响更加严重,因为每一次的网络请求都有一定概率丢包的可能,所以请求越多丢包的概率越大;keep-alive方式本身也存在一些问题:请求在经过代理服务器时链接有可能会断开,他很难保持keep-alive在整个请求过程中的状态。
    也不能说合并文件就是万能的,合并文件本身也存在其自身的问题:
    第一是受屏渲染的问题,当进行了文件合并后,JavaScript文件尺寸肯定会比合并之前大,在进行HTML网页渲染时,必须要等待文件请求加载完成后才能进行渲染。当经过合并后的JavaScript文件非常大且请求时间比较久时,页面渲染过程就会遭受比较久的延迟。
    第二是缓存失效的问题,因为目前大部分项目都有缓存策略,即每个请求的JavaScript文件都会加一个md5的戳,用来表示文件是否发生修改更新,当发现文件被修改时,就会让缓存失效重新请求文件。如果只发生了一处很小的修改,则没进行合并时只有修改的文件失效,而若进行了文件合并,就会造成大面积的缓存失效。
  2. 使用建议
    基于上述优劣势分析,这里给出在进行文件合并时的考虑建议:首先是合并公共库,通常我们的前端代码会包含自己的业务逻辑代码和引用的第三方公共库代码,业务逻辑代码的修改变动会比公共库代码修改频繁,所以可将公共库代码打包成一个文件,而对业务代码进行单独处理。
    其次就是对业务代码按照不同页面进行合并,目前大部分前端都是SPA,我们所期望的是仅当前页面被路由请求到后,才去加载对应页面的JavaScript文件及相关资源,这就需要对不同页面的文件进行单独打包。
  3. 如何合并
    前端工程化基本都会在构建层使用相应的构建工具进行文件合并,常见的构建工具有gulp、fis3和webpack。

6.2 使用fis3进行前端构建

fis3是百度推出的一款面向工程构建工具,用来解决前端工程化中的性能优化、资源加载(异步、同步、按需、预加载、依赖管理、文件合并与内嵌)、模块化开发、自动化工具、开发规范及代码部署等问题。fis3对比webpack来说更小巧且易上手,适合规模较小的项目。

6.2.1 构建流程
fis3的整个构建流程首先从start文件开始,根据正则表达式和指定的一些中间码规则对源文件进行分析,比如分析JavaScript文件内部的依赖,从而得到相关的依赖树。在得到所有文件的依赖树后,会通过fis.compile方法对单个文件进行编译;编译完成后,会根据所定义的打包规则进行打包。从整体上看,可以简单将其构建流程划分未两部分:单文件编译过程与打包过程。
在这里插入图片描述

6.3 使用webpack进行前端构建

webpack是目前绝大部分的前端项目都在使用的构建工具。使用范围比fis3更加广泛。

6.3.1 模块打包工具
webpack的本质是一款模块打包工具,它的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用。随着前端功能越来越复杂,显然不可能将所有JavaScript代码都写在一个文件里,这样的代码可读性和可维护性都会很差,所以需要将一些功能相对独立的模块拆出来分别放到不同文件。webpack就是帮助我们完成这种功能的工具。

6.3.2 webpack核心概念

  1. 入口(entry)
    入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
  2. 输出(output)
    output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。
  3. loader
    webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
  4. 插件(plugin)
    loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
  5. 模式(mode)
    通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。

6.4 webpack优化性能

webpack的优化瓶颈主要体现在两方面:打包构建过程太浪费时间;打包结果体积太大。
对大部分前端项目来说,每次修改调试都有可能需要对全部或部分的代码进行打包构建,可想而知如果这个过程十分耗时,将会非常影响前端工程师的开发效率,并且如果打包结果过大,必然也让HTTP的单次请求花费过长时间。

6.4.1 尽量与时俱进
webpack每个版本更新时,其内部肯定会进行相应的优化,当更新了webpack得版本后,其打包构建速度便会相应得到提升。

6.4.2 减少Loader的执行
根据具体情况使用include或exclude,在尽可能少的模块上执行Loader。

const path = require('path');
module.exports = {
    entry: { //入口文件
        main: './src/index.js'
    },
    module: {
        rules: [{
            test: /\.js$/,   //针对除node_modules文件夹路径之外的JavaScript
            exclude: /node_modules/,
           use: [{ loader: 'babel-loader' }],
        },{ //对于图片文件的打包规则
            test: /\.(jpg|png|gif)$/,
            use:{
                loader: 'url-loader',
                options: {                       //url-loader对图片的处理配置
                    name: '[name]_[hash].[ext]', //输出文件名
                    outputPath: 'images/',       //输出路径
                    limit: 10240,                //大小限制
                }
            }
        }]
    }
    output: { //输出位置
        filename: "bundle.js",
        path: path.resolve(_dirname,'dist');
    }
}

这里关注module字段中对JavaScript文件的处理规则,如果不加exclude字段,则webpack会对该配置文件所在路径下的所有JavaScript文件使用babel-loader,虽然babel-loader的功能强大,但它的执行速度很慢。
这样处理JavaScript还会涉及到node_modules路径下项目引用的所有第三方文件。由于第三方库的文件在发布前本身已经执行过一次babel-loader,所以不用重复执行一次,故此exclude字段不可省略。
与之对应的还有一个include字段,使用含义与exclude相反,即仅对其指定范围的JavaScript文件进行处理,以降低loader被执行的频率。
对于图片文件则没有必要通过include或exclude来降低loader的在执行频率,因为无论哪里引入的图片,最后打包都需要通过url-loader对其进行处理,所以include或exclude的语法并不适用于所有loader类型,要根据具体的情况而定。
另外,如果开启缓存将构建结果缓存到文件系统中,则可让babel-loader的工作效率得到成倍增加,处理方式也很简单,增加一个参数:

loader: 'babel-loader?cacheDirectory=true'

6.4.3 确保插件的精简和可靠
对于有必要使用插件的情况,建议使用webpack官方网站上推荐的插件,因为该渠道的插件性能往往经过了官方测试,如果使用未经验证的第三方公司或个人开发的插件,其性能没有保证,可能会导致整体打包质量的下降。

6.4.4 合理配置resolve参数
配置resolve参数可以为我们在编写代码引入模块时提供不少便利,比如使用extensions省略引入JavaScript文件的后缀,使用alias减少书写所引入模块的多目录层级,使用mainFiles生命目录下的默认使用文件等,但当我们使用这些参数带来便利的同时,如果滥用也会降低打包速度。
resolve通过对mainFiles的篇日志可以指定让webpack查找引入模块路径下的默认文件名,虽然他能在很大程度上简化模块引用的编码量,但付出的代价是增加了打包构建过程中对目标文件的查找时间,所以不建议使用。

6.4.5 将单进程转化为多进程
我们可以使用happypack充分释放CPU在多核并发方面的优势,帮助我们把打包构建任务分解成多个子任务去并发执行,这将大大提高打包的效率。其使用方法也很简单,就是将原有的loader配置转移到happypack中去处理:

//引入happypack
const happypack = require('happypack');
//创建进程池
const happyThreadPool = Happypack.ThreadPool({ size: os.cpus().length })
module.exports = {
    modules: {
        rules: [
            ...
            {
                test: /\.js$/,
                // 指定处理这类文件及相应happypack的实例
                loader: 'happypack/loader?id=happyBabel',
                ...
            },
        ],
    },
    plugins: [
        new Happypack({
            id: 'happyBabel',            // 对应规则中的'happyBabel',表示实例名
            threadPool: happyThreadPool, // 指定线程池
            loaders: ['babel-loader?cacheDirectory']
        })
    ]
}

6.4.7 压缩打包结果的体积

  1. 删除冗余代码
  2. 代码拆分按需加载

6.5 小结

HTTP的优化有两大方向:减少请求次数、减少单次请求所花费的时间。这就是前端工程化做的事情——资源的压缩与合并。
希望读者记住:进行性能优化的过程中,使用了什么工具,如何使用其实并不重要,关键是需要明白这样的优化是出于什么原理的考虑,解决了怎样的性能痛点。

前端性能优化系列:
前端性能优化:1.什么是前端性能优化
前端性能优化:2.前端页面的生命周期
前端性能优化:3.图像资源优化
前端性能优化:4.资源加载优化
前端性能优化:5.高性能的JavaScript代码
前端性能优化:6.项目构建优化
前端性能优化:7.页面渲染优化

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值