前端性能优化-CSS内联

461 阅读5分钟

先看实践效果

image.png

一、业务背景:

  1. 秒开率目标
  2. 目前运营活动中的CSS文件是犬粮打包进js文件中
    1. 优点:是方便管理,减少首次请求数量
    2. 缺点:首次加载时需要下载整个JS文件增加首次加载时间,以及无法使用css缓存;不能和js并行下载;
    3. 尤其目前我们业务中有一个比较大的公共style文件,页面每个组件中都需要单独引入;
    4. 项目中没有进入css的treeShaking;

针对html没进行压缩的问题,我们引进html的压缩机制

const minifyHtmlPlugins = (pages) => {
  return Object.values(pages).map((page) => {
    return new HtmlWebpackPlugin({
      template: page.template,
      filename: page.filename,
      minify: {
        minifyCSS: true, // 压缩内联CSS
        minifyJS: true, // 压缩内联JS
        collapseWhitespace:true,
        keepClosingSlash:true,
        removeComments: true, // 移除注释
        removeRedundantAttributes:true,
        removeScriptTypeAttributes:true,
        removeStyleLinkTypeAttributes:true,
        useShortDoctype: true
      },
    })
  })
}

  configureWebpack: config => {
    config.stats = "normal";
    config.module.rules.push({
      test: /banners/[\w$/]+/main.js$/,
      use: ["./src/plugins/release.loader.js", "./src/plugins/hoc.loader.js"]
    });
    config.plugins.push(...minifyHtmlPlugins(pages))
  }

二、优化方向

1.`src/style`中的公共样式打包成一个单独的css文件压缩并上传到CDN,link到html中进行预加载;
2.  设置全局stylus变量
3.  css拆分,首屏css内联(Critial CSS);其他css延迟加载(或者按需加载)
4.  非首屏css延迟加载(或者按需加载)
5.  引入CSS的treeShaking

三、 具体实施

1. 公共CSS上传CDN并进行预先加载

在项目中,我以在css单独抽离css中的extract配置打开打包出来的结果搜索全局公共样式fz-28里面有8个;整个css文件有381KB; 通过查看代码发现,我们在项目中是这样使用style的

// main.js中
import { setFontSize } from '@/rem';
import '@/directive/preventReClick';
import '@/style/wepie.styl'

// 每个组件的style中
@import '~@/style/stylusSet/index.styl';
.btn-rule
  wh 100
  left 7.7rem
  right 1rem

// 但是在'~@/style/stylusSet/index.styl'中又引入了原生重算
// 原生重算
@import "./reset.styl"
@import "../wepie.styl";
@import "../partials/reset.styl"
@import "../easy.styl"

所以也就是最终我们引用n次~@/style/stylusSet/index.styl,加上main.js中的1个,最终重复的公共代码的总次数就是n+1

优化一:改写重制部分部分引入;

所以第一个优化就是去除~@/style/stylusSet/index.styl中的原生重算部分;改用只在main.js中引入;这时重新打包后发现大小缩减至270KB

优化二:全局公共代码上传CDN,并进行预加载

image.png

2. 设置全局stylus变量

项目中用的stylus,里面封装了一套适配项目的一些简化css编写的函数,如设置背景图的工具函数如下:

image.png

在使用时,我们需要在页面中引入该函数,如下所示

image.png

每次都要引入; 可以释放这部分工作

module.exports = {
    css: {
        ...,
        loaderOptions: {
            stylus: {
                 // 设置全局stylus变量
                import: [
                  '~@/style/stylusSet/index.styl',
                ]
            }
        }
    }
}

3. CSS拆分,主屏CSS内联

但是问题是从js中剥离后,意味着页面加载需要多发一个http请求;所以想到另外一个替代方式,就是css内联

CSS内联需要使用html-inline-css-webpack-plugin

【原理】: 将CSS打包成单独Chunk; 遍历所有的css-chunk,将其复制设置index.html的header头下(默认);然后(默认)删除掉所有的css-chunk;

这种简单粗暴的实现方式有几个隐藏的问题,后面讲到

3.1 引入时要注意有default

const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
  • 官方文档webpack中配置如下 image.png

3.2 在vue-cli中写法

但是实际在我们的vue-cli中并不能这样配置,因为里面已经有很多默认配置了;可以通过vue inspect > webpack.config.js查看

  • 必须将css打包成单独chunk
module.exports = {
    css: {
        extract: true,
    }
}

多页面HtmlWebpackPlugin优化处理

const minifyHtmlPlugins = (pages) => {
  return Object.values(pages).map((page) => {
    return new HtmlWebpackPlugin({
      template: page.template,
      filename: page.filename,
      chunks: page.chunks,
      inject: true,
      minify: {
        html5: true,
        minifyCSS: true, // 压缩内联CSS
        minifyJS: true, // 压缩内联JS
        collapseWhitespace:true,
        keepClosingSlash:true,
        removeComments: true, // 去除注释
        removeRedundantAttributes:true,
        removeScriptTypeAttributes:true,
        removeStyleLinkTypeAttributes:true,
        useShortDoctype: true
      },
    })
  })
}

css内联到html中

module.exports = {
  configureWebpack: config => {
    ...,
    // 单独提取css文件(这里添加后会导致HTMLInlineCSSWebpackPlugin中有2份样式)
    // new MiniCssExtractPlugin({
    //   filename: "[name].css",
    //   // chunkFilename: "[id].css"
    // }),
    config.plugins.push(...[
         // 压缩html以及里面的css/js等
        ...minifyHtmlPlugins(pages),
        // 将单独提取的css文件内联到html中(这行要放在HtmlWebpackPlugin后)
        new HTMLInlineCSSWebpackPlugin()
    ])
  }
}

上述注视注意,vue-cli.js中通过开启css.extract = true时已经处理了MiniCssExtractPlugin; 这里如果再写,将会导致html中copy两份一样的css;所以不要再写;

3.3 问题第一弹

通过build本地打包,并启动http-server后,预览页面,发现页面中样式图片路径均是404

审查html中的样式代码发现如下

image.png

里面图片路径均是../

这里我们执行vue inspect --mode product > webpack.config.js

image.png 查看到css-chunk默认生成路径在css文件夹下

image.png 同时查看,默认设置的单独提取css中设置的publicPath为'../'

这里我这边做测试修改; 直接在plugin中试图覆盖

image.png

再次执行vue inspect --mode product > webpack.config.js命令查看改动没有生效;所以得换方式

module.exports = { 
    chainWebpack: config => {
        config.plugin('extract-css')
        .use(MiniCssExtractPlugin, [
            {
                filename: '[name].[contenthash:8].css',
                chunkFilename: '[name].[contenthash:8].css'
            }
       ]);
   }
 }

再次执行vue inspect --mode product > webpack.config.js

image.png

已经更新成功

BUT 执行build时报错:Error: No module factory available for dependency type: CssDependency; 原因待查……

这里尝试不改变css生成的路径,直接改css中引入的背景图路径的法子; 如上直接改publicPath

module.exports = { 
    chainWebpack: config => {
        config.module
          .rule('stylus')
          .oneOf('vue')
          .use('extract-css-loader')
          .tap(options => {
            options.publicPath = '/'
            return options;
          });
      }
    }
}

执行vue inspect --mode product > webpack.config.js 和执行build后图片路径变成如下

image.png

image.png

4. 非首屏css按需加载

按需加载需要vue写的时候用组件异步加载的写法写;

const GuardTreasure = () => import('./components/treasure/guard-treasure.vue')

这样引入的css文件会被link到JavaScript文件中,在运行时需要时加载,而不会inline到html中;

4.1 问题第二弹

但是我们这样写了后发现异步组件加载时会报https://xxxxx/css/chunk-0950ed4p.css404;

查找原因就是因为先前提到的html-inline-css-webpack-plugin原理中,在copy了依赖的CSS后,在删除时是将整个CSS文件夹删除,导致异步组件中link的css-chunk也被意外删除;

看源码以及官网说明,我们绝对直接保留css文件夹;这样唯一的后果就是打包后的dist/css中多了一些已经被inline到html中的无用样式,浪费存储,但其实并不会引入和读取,没有其他副作用了; 所以我们要设置一个属性

      new HTMLInlineCSSWebpackPlugin({
        leaveCSSFile: true,
      }),

5. CSS的Tree Shaking 擦除无用的样式

const PurgeCSSPlugin = require('purgecss-webpack-plugin')
module.exports = { 
    chainWebpack: config => {
    config.plugin.push(
        new PurgeCSSPlugin({
            paths: globAll.sync(
              [...Object.keys(pages).map(pageDirName => `${path.join(__dirname, '../src')}/banners/${pageDirName}/**/*`),`${path.join(__dirname, '../src')}/comps/**/*`],
              { nodir: true }
            ),
            // paths: glob.sync(`${path.join(__dirname, '../src')}/banners/${pageDirName}/**/*`,  { nodir: true }),
            whitelist: ['webp'], // 白名的 
            whitelistPatterns: [/^kp/], // 以kp-开头的 不擦除
      })
    )

   }
 }