Skip to content

白屏优化

优化背景 (加载时间对比)

null

为什么需要优化白屏(FCP)时间

  • 增强用户体验
  • 提升页面留存率

分析原因

1. 二者区别

  • 点播后台使用Vue-Cli 4,而直播后台使用Vue-Cli 5
  • 在加载流程上,点播后台共有==295==次请求而直播后台仅==53==次请求,其中,==295==有200+是细碎的chunks文件

2. 原因

  • 细碎chunks提前请求阻塞了页面资源的请求与加载,这些文件都通过prefetch进行预加载,导致其他资源请求延后

  • chunks体积很小,但是数量过多,而浏览器一般对同一个==域名==的请求有所限制(Chrome一般不多于6个),导致等待200多个请求需要花费大量时间。

    ::: tips 一个补充

    Chrome确实是对同一个域名下的并发请求的个数是有限制的,不同的GET/POST请求的限制为6个(使用HTTP/2.0+ 或者 HTTPS即可突破),而==同一个==GET请求,会依次一个一个的执行。

    但是当请求静止等待==20s==后,这些剩余的相同GET请求照样会并发。这里的原因在于:Chrome浏览器的==get请求缓存==,对于同一个URL+相同参数向后台发送请求的时候,如果前一个请求未执行后一个请求将被阻塞,而阻塞的时间最长为==20s==。

    :::

  • 有的JS文件没有被压缩

解决方案

1. 针对细碎chunks prefetch 的情况

TIP

  • 什么是prefetch

    预提取(prefetch)是一种浏览器机制,利用浏览器的==空闲时间==加载用户在将来的不久可能会访问到的资源,并且缓存到内存中。

    在实际调用的时候,可以节省资源加载的时间。

  • 为什么会出现prefetch

    原因在于Vue-Cli 4默认开启了PreloadPlugin插件,他会把资源的link添加html中,并且添加上rel="prefetch",而Vue-Cli 5是默认关闭的。

    image-20230504214109485

  • 为什么prefetch会在页面资源加载前去加载呢 🕵️

    这与浏览器的加载机制相关,空闲时间实际上指的是==高优先级任务的间隔时间==,在这个时间会用来加载低优先级任务。这个时间并不是我们所想的等所有高优先级任务加载好后再进行加载。

    image-20230504214337410

方案:

  1. 直接关闭preFetch(与Vue-Cli 5一样)
  2. 利用定时器,将加载的顺序按照我们所想的那样做

这里说一下如何实现第二种方法。

如何判断DOM是否已经加载成功?

以下方法能够成功获取到DOM元素后,说明页面加载完成了

js
addEvent(window, "load", function () {
	// ...
})

document

document.getElementByTagName('...')
document.getElementById('...')
document.querySelector('...')

document.body
addEvent(window, "load", function () {
	// ...
})

document

document.getElementByTagName('...')
document.getElementById('...')
document.querySelector('...')

document.body
  • 通过轮询判断是否能够获取到DOM,如果已经获取到某个DOM元素,说明加载完成既可以加载prefetch资源
  • DOM元素必须每个页面都包含
js
//延迟加载prefetch资源
let timer =  setInterval(function() {
    //由于.p-live-admin-app被获取到时页面已加载完成,
    //通过轮询判断是否有该元素来判断页面是否加载完成
    //从而加载prefetch 资源
	const lastChild =  document.querySelector('.p-live-admin-app');
    if (lastchild){
        loadChunks({
        chunks:['']
        assetTypes:['css','js'],
        prefetch:true,
        allChunkData
    	});
        window.clearInterval(timer);
        timer = null;
	}
},3000);
//延迟加载prefetch资源
let timer =  setInterval(function() {
    //由于.p-live-admin-app被获取到时页面已加载完成,
    //通过轮询判断是否有该元素来判断页面是否加载完成
    //从而加载prefetch 资源
	const lastChild =  document.querySelector('.p-live-admin-app');
    if (lastchild){
        loadChunks({
        chunks:['']
        assetTypes:['css','js'],
        prefetch:true,
        allChunkData
    	});
        window.clearInterval(timer);
        timer = null;
	}
},3000);

2. 针对过多细碎js/css

方案:

通过WebPack插件在打包时把细碎的jscss合并。

通过MinChunkSizePlugin插件,合并小于minChunkSize的文件保持chunk的最小大小,单位是Byte

js
// webpack.config.js

// ...
this.minChunkSizePlugin = new webpack.optimize.minChunkSizePlugin({
    minChunkSize: this.minChunkSize,
})
// webpack.config.js

// ...
this.minChunkSizePlugin = new webpack.optimize.minChunkSizePlugin({
    minChunkSize: this.minChunkSize,
})

3. 针对某些JS文件没有压缩

方案

  • 手动使用指令压缩代码,并且手动inline
  • 自定义插件实现每次打包的时候自动操作
压缩: terser | uglify
js
// terser
const fs = require('fs');
const { minify } = require('terser');
const result = minify(fs.readFileSync(file, 'utf-8'));

// uglify
const fs = require('fs');
const { minify } = require('uglify-js');
const result = minify(fs.readFileSync(file, 'utf-8'));
// terser
const fs = require('fs');
const { minify } = require('terser');
const result = minify(fs.readFileSync(file, 'utf-8'));

// uglify
const fs = require('fs');
const { minify } = require('uglify-js');
const result = minify(fs.readFileSync(file, 'utf-8'));
内联:获取HTML后将JS放入body后的script标签中
js
inlineJs(optionPath, htmlData) {
    const str = minify(fs.readFileSync(optionPath, 'utf-8'), {}).code || ''; // optionPath为需要压缩内联的JS的路径
    const scriptCode = `<script>${str}</script>` || '';
    const htmlStr = htmlData.html.toString();
    return htmlStr.replace(new RegExp('</body>', 'g'), scriptCode + '</body>'); // 内联至body最后面
    
}
inlineJs(optionPath, htmlData) {
    const str = minify(fs.readFileSync(optionPath, 'utf-8'), {}).code || ''; // optionPath为需要压缩内联的JS的路径
    const scriptCode = `<script>${str}</script>` || '';
    const htmlStr = htmlData.html.toString();
    return htmlStr.replace(new RegExp('</body>', 'g'), scriptCode + '</body>'); // 内联至body最后面
    
}
处理时机:在html-webpack-pluginhtmlWebpackPluginAfterHtmlProcessing钩子中获取html
js
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap
('GenTemplatePlugin',htmlData => {
    if (htmlData.plugin.options.meta.moreTemplateDisabled) return;
	htmlData.html = this.removeLink(htmlData);
	htmlData.html = this.inlineJs(this.loadPath, htmlData);
});
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap
('GenTemplatePlugin',htmlData => {
    if (htmlData.plugin.options.meta.moreTemplateDisabled) return;
	htmlData.html = this.removeLink(htmlData);
	htmlData.html = this.inlineJs(this.loadPath, htmlData);
});

代码总结

js
const AssetsPlugin = require('assets-webpack-plugin');
const webpack = require('webpack');
const fs = require('fs');
const minify = require('terser');

class GenTemplatePlugin {
    constructor(options) {
        this.options = options || '';
        this.minChunkSize = this.options.minChunkSize;
        this.loadPath=this.options.loadPath; // loadAssets.js文件的位置
		this.outputPath=this,ptions.outputPath; // 生成多模板加截文件的位置
        this.filename=this.options.filename;// 生成多模板加载文件的名称
		this.resources=this.options.resources;// 除chunk外的资源文件
		this.minChunkSizePlugin = new webpack.optimize.MinChunkSizePlugin({
    		minChunkSize: this.minChunkSize,
		});
		this.assetsPlugin = new AssetsPlugin({
    		filename:this.filename,
    		prettyPrint:false,
    		path:this.outputPath,
    		includeAllFileTypes:false
        });
    }
}
const AssetsPlugin = require('assets-webpack-plugin');
const webpack = require('webpack');
const fs = require('fs');
const minify = require('terser');

class GenTemplatePlugin {
    constructor(options) {
        this.options = options || '';
        this.minChunkSize = this.options.minChunkSize;
        this.loadPath=this.options.loadPath; // loadAssets.js文件的位置
		this.outputPath=this,ptions.outputPath; // 生成多模板加截文件的位置
        this.filename=this.options.filename;// 生成多模板加载文件的名称
		this.resources=this.options.resources;// 除chunk外的资源文件
		this.minChunkSizePlugin = new webpack.optimize.MinChunkSizePlugin({
    		minChunkSize: this.minChunkSize,
		});
		this.assetsPlugin = new AssetsPlugin({
    		filename:this.filename,
    		prettyPrint:false,
    		path:this.outputPath,
    		includeAllFileTypes:false
        });
    }
}

Released under the MIT License.