前端性能优化基础

279 阅读34分钟

第一章 性能指标

1. RAIL测量模型

⭐️R:response用户与浏览器交互时的响应时间

⭐️A:animation浏览器动画

⭐️I:idle浏览器的空闲时间

⭐️L:load加载时间

2. RAIL测量标准

⭐️响应:处理事件在50ms以内完成

⭐️动画:每10ms产生一帧

⭐️空闲:尽可能增加空闲

⭐️加载:在5s内完成所有内容的加载,可以进行交互

第二章 性能检测

1. NetWork

  • 浏览器瀑布流,查看请求数量、资源大小等信息

  • 请求瀑布流

  • 请求总量、请求资源大小、DOM加载时长、总加载时长

  • size大小,Time时间

注意⚠️:size分为网络传输大小和文件真实大小

2. Lighthouse

浏览器整体质量评估,同时会返回一些优化建议

  • npm install -g lighthouse(只针对移动端)
  • 浏览器开发者工具(可以选择移动端还是web)

1)整体性能评估

2)各项指标评估解读

3)白屏时长

4)优化建议

5)诊断结果

3. performance

  • 检测用户执行某段操作时的性能
  • 检测页面初始加载的性能

1)报告分析

2)帧率

粉色:帧数已经下降到影响用户体验的程度,需要优化 绿色:Fps指数,绿色柱体高度越高,性能越好 红色块:页面渲染卡顿,需要优化

3)cpu资源

消耗 CPU 资源的事件类型

4. WebPageTest

第三方软件,评估网站整体性能

官网:www.webpagetest.org/

  • 模拟用户所在地址
  • 模拟用户的网络环境
  • 模拟测试不同浏览器
  • 自动设置测试次数,求平均值
  • 可以区分首次访问和非首次访问

线上网站:直接利用官网检测 开发环境:通过docker在本地安装一个webpagetest(百度相关教程)

First view首次访问 Repeat View 二次访问

5. performance对象属性

对外暴露出浏览器内部性能的度量结果,导出了页面加载和渲染过程的很多信息 Web计时机制的核心是window.performance对象

1)memory

  • jsHeapSizeLimit:内存大小限制
  • totalJSHeapSize:可使用的内存
  • usedJSHeapSizejavascript:JS对象占用的内存

2)navigation

  • redirectCount:页面加载前的重定向次数
  • type:数值常量,表示刚刚发生的导航类型(0:初次加载,1:页面重载,2:页面通过前进或后退打开的)

3)timing

performance.timing属性是一个对象,对象的属性都是时间戳,每个时间戳代表不同的事件

4)时间节点事件

// 白屏时间 
timeList.reponseEnd - timeList.fetchStart
// 首次可交互时间 
timeList.domInteractive - timeList.fetchStart
// DOM Ready时间 
timeList.domContentLoadEventEnd - timeList.fetchStart
// 页面完全加载时间
timeList.loadEventStart - timeList.fetchStart
  • navigationStart:开始导航到当前页面的时间,即在地址栏输入地址后按下回车时
  • redirectStart:到当前页面的重定向开始的时间(重定向的页面来自同一个域)
  • redirectEnd:到当前页面的重定向结束的时间(重定向的页面来自同一个域)
  • fetchStart: 开始通过HTTP GET取得页面的时间
  • domainLookupStart:查询当前页面DNS的开始时间,如果使用了本地缓存或持久连接,则与fetchStart值相等
  • domainLookupEnd:査询当前页面DNS的结束时间,如果使用了本地缓存或持久连接,则与fetchStart值相等
  • connectStart:浏览器尝试连接服务器的时间
  • secureConnectionStart:浏览器尝试以SSL方式连接服务器的时间
  • connectEnd:浏览器成功连接到服务器的时间
  • requestStart:浏览器开始请求页面的时间
  • responseStart:浏览器接收到页面第一字节的时间
  • responseEnd:浏览器接收到页面所有内容的时间
  • unloadEventStart:前一个页面的unload事件开始的时间(同域)
  • unloadEventEnd:前一个页面的unload事件结束的时间(同域)
  • domLoading:开始解析DOM树的时间
  • domInteractive:完成解析DOM树的时间
  • domContentLoadedEventStart:开始加载网页内资源的时间
  • domContentLoadedEventEnd:网页资源加载完成的时间
  • domComplete:DOM树解析完成、网页内资源准备就绪的时间
  • loadEventStart:load回调函数开始执行的时间
  • loadEventEnd:load事件已经发生且执行完所有事件处理程序的时间
// DNS解析耗时 
timeList.domainLookupEnd - timeList.domainLookupStart
// TCP连接耗时 
timeList.connectEnd - timeList.connectStart
// SSL安全连接耗时 
timeList.connectEnd - timeList.secureConnectionStart
// 网络请求耗时(TTFB)
timeList.responseStart - timeList.requestStart
// 数据传输耗时 
timeList.reponseEnd - timeList.reponseStart
// DNS解析耗时 
timeList.domInteractive - timeList.reponseEnd
// 资源加载耗时 
timeList.loadEventStart - timeList.domContentLoadedEventEnd
// First Byte时间 
timeList.reponseStart - timeList.domainLookupStart
// 白屏时间 
timeList.reponseEnd - timeList.fetchStart
// 首次可交互时间 
timeList.domInteractive - timeList.fetchStart
// DOM Ready时间 
timeList.domContentLoadEventEnd - timeList.fetchStart
// 页面完全加载时间 
timeList.loadEventStart - timeList.fetchStart

6. Memory堆快照

image.png

点击Take snapshot可以捕获多张堆快照,前后对比分析堆内存变化情况,进而分析内存泄漏的场景。

image.png

  • Constructor:使用Constructor构造函数创建的对象
  • Distance:对象最短简单路径到GC根节点(被直接和间接引用的对象集合)的距离
  • Shallow Size:由特定构造函数创建的所有对象的浅层大小总和。浅层大小是对象本身占用的内存大小,典型的 JavaScript 对象会预留一些内存用于说明和存储值。通常,只有数组和字符串具有明显的浅层大小。不过,字符串和外部数组的主存储空间通常位于渲染器内存中,只在 JavaScript 堆上公开一个小的封装容器对象。
  • Retained Size:同一组对象中的最大保留大小。这是对象本身已经无法再从GC根访问的依赖对象后被释放的内存大小。

任何无法从GC根到达的内容都会被 GC(垃圾回收)

排查内存泄漏可以借助自定义的类,假设自定义一个类Person,拍摄快照,筛选找到Person,查看引用情况,进而分析泄漏原因。

image.png

image.png

第三章 性能优化

1. 浏览器渲染优化

  • JavaScript:触发视觉变化(分割图层)
  • Style:计算每个图层节点的样式
  • Layout:为每个节点生成图层和位置
  • Paint:将每个节点绘制填充到图层位图中
  • Composite:组合多个图层到浏览器页面上形成最终的屏幕图像(图层重组)

1)重绘与回流

回流:当render tree中某些元素的尺寸、布局、隐藏方式等发生改变时,浏览器需要重新构建渲染树,这种情况就叫做回流(重排或布局),其他特点是页面布局和几何属性改变

重绘:当render tree中某些元素的外观、风格属性发生变化时,浏览器需要重新绘制渲染树,这种情况叫做重绘,其特点是外观样式属性改变,布局不会改变

重绘不一定导致回流,回流一定会导致重绘。

2)触发回流的css属性

  • 盒子模型相关属性
宽度:width 
高度:height
内边距:padding 
外边距:margin 
元素表现形式:display 
边框宽度:border-width 
最小高度:min-height
  • 定位、浮动相关属性
定位:position 
位置定位:topleftbottomright 
浮动:float 
清除浮动:clear
  • 文字结构相关属性
文本居中:text-align 
滚动:overflow-y 
字体加粗:font-weight 
字体:font-family 
字体行高:line-height 
字体间距:white-space 
字号大小:font-size

2)触发重绘的css属性

字体颜色:color 
边框样式:border-style 
圆角:border-radius 
显示隐藏:visibility 
背景色:background 
背景图片:background-image 
背景定位:background-position 
背景图片大小:background-size 
阴影:box-shadow

3)优化回流重绘

避免使用可以导致重绘和回流的css属性

  • translate 代替 top
  • 合并css样式修改
  • 不要把DOM节点的属性放到一个循环里,作为循环变量(offsetWidth,offsetHeight)
  • 不使用table布局
  • 为动画新建图层、启用GPU加速

将频繁的重绘和回流元素单独放置在一个图层中,限制其影响范围。但是图层重组会严重影响浏览器性能,所以不建议创建过多的图层

4)创建图层

  • css属性(perspective transform)
  • 视频解码<video>标签
  • WebGL、canvas
  • Flash动画
  • 设置css3D过滤器的元素,transform:translateZ(0)

查看所有图层 浏览器开发者工具中的Layers可以查看所有图层

Compositing Reasons:成为单独图层的原因分析 搭配console drawer中Rendering时时检测

2. JS代码优化

1)打包及加载优化

  • js减重Tree shaking、拆分打包
  • js按需加载

2)解析执行优化

  • 避免单个任务过长
  • 多线程同步执行

3)JS引擎优化

浏览器的js引擎工作原理大致可以分为以下步骤:

第一步:js源码 => 抽象语法树 => 字节码 => 机器码

第二步:编译器在编译过程中会进行自动优化

第三步:执行过程中会进行反优化

字节码会进行缓存,所以执行过程中需要不断更新字节码。

针对字节码缓存的优化方案:代码要尽量符合编译器的优化规则,避免反优化

js引擎工作原理

const num1 = 1;
const num2 = 2;
const add = (a,b) => a+b

for(let i =0,i< 100,i++){
    add(num1,num2); // 编译器认为这里的add就是两个数字之间的运算
}

add(3, '你好'); // 编译器发现之前的想法是错的,就取消了之前自己的想法

for(let i =0,i< 100,i++){
    add(num1,num2); // 编译器执行到这里的时候没有默认值,每次都当作一个新的函数执行
}

3. 函数解析优化

JS引擎对于函数默认采用的是懒解析,而立即执行函数会经历两次解析:懒解析和饥饿解析,造成资源浪费,降低性能。

// 代码中标明一个函数是懒解析还是饥饿解析
const add = ((a,b) => a+b) // 添加一对(),变为饥饿解析

4. HTML优化

  • 减少iframe文档流的使用
  • 压缩空白符
  • 避免深层次的嵌套(DOM树层次太深,影响性能),例如无意义的外层div
  • 避免table布局
  • 删除注释
  • css和js尽量引用外部文件
  • 删除元素默认属性
  • 可以使用语义化标签

5. CSS优化

  • 降低css对渲染的阻塞,如按需加载css文件,必要css文件提前加载进行解析
  • 使用GPU完成动画
  • font-display属性,控制字体加载和替换
  • 使用contain属性隔离元素,如contain:layout

contain属性:隔离指定内容的样式、布局、渲染,类似于iframs,浏览器在重新计算布局、样式、绘图、大小或这四项的组合时,只影响到有限的 DOM 区域,而不是整个页面,可以有效改善性能,该属性不可继承

contain: none | strict | content | size | layout | paint
  • strict:具有复杂结构的元素,其子元素经常发生变化,添加该属性后元素的样式和布局不会受到其子元素的影响,减少浏览器重新计算样式和布局的次数
  • size:已知元素的尺寸且不会发生变化,减少浏览器重新计算布局的次数
  • content:某个元素的样式和布局不受其子元素的影响,但受到后代元素的影响

6. 图片优化

1)图片格式

  • JPEG/JPG:有损压缩,边缘有锯齿感,适合首页轮播图
  • PNG:体积较大,边缘感不明显,适合logo图标
  • svg:可缩放矢量图形,自定义颜色、大小,适合图标

2)图片加载优化

  • 原生懒加载 <img loading="lazy" src="转存失败,建议直接上传图片文件 " alt="转存失败,建议直接上传图片文件">
  • 第三方懒加载插件 React-lazy-load-image-component
  • 渐进式图片(UI支持)像素由低到高,逐渐清晰

3)响应式图片

srcset属性表示根据屏幕尺寸加载对应大小的图片

<img 
    src="light-200.jpg" 
    sizes="100vw" // 图片大小
    srcset="light-100.jpg 100vw,light-200.jpg 200vw, light-400.jpg 400vw, light-600.jpg 600vw"
>

7. 图标优化

1)Icon-font

优点:

  • 将多个图标合并为一个字体库,减少请求次数,但是有可能增大资源大小
  • 矢量图形,可以自动伸缩变换
  • 直接通过css,style改变其外观

缺点:

  • 缺少色彩、内容多样性
<Link rel="" href="./iconfont.min.css">
<i class="iconfont-adress-book"></i>

2)svg

优点:

  • 保持了图片的能力,支持多色彩
  • 独立的矢量图形
  • XML语法,搜索引擎可以更好的解读

8. Webpack打包优化

webpack是基于nodejs的javaScript应用的静态模块化打包工具

1)重要概念

Entry:入口文件,webpack编译的起点

Compiler:编译管理器,webpack 启动后会创建 compiler 对象,该对象一直存活到编译结束

Compilation:单次编译过程的管理器,每次触发重新编译时,都会创建一个新的compilation对象

Dependence:依赖对象,webpack 基于该类型记录模块间依赖关系

Module:webpack内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的

Chunk:编译完成准备输出时,webpack 会将 module 按特定的规则组织成一个一个的 chunk,这些 chunk 某种程度上跟最终输出一一对应

output:出口,webpack输出文件。其中output.filename对应initial chunk文件名称,output.chunkFilename对应non-initial chunk文件名称

Loader:资源内容转换器,读取、转译资源,理论上输出标准JavaScript文本或者 AST 对象

Plugin:webpack构建过程中,会在特定的时机广播对应的事件,插件会监听这些事件,并在特定的时间点介入编译过程。从形态上看,插件通常是一个带有 apply 函数的类,apply函数运行时会得到参数 compiler

class SomePlugin {     
  apply(compiler) {} 
}

webpack入口

项目中每个文件都是一个模块,在打包过程中,模块会被合并成 chunk,chunk 合并成 chunk 组,通过模块互相连接的关系从而形成图;

  • 单入口
module.exports = {   
  entry: './index.js', 
}

创建一个名为 main 的 chunk 组(main 是入口起点的默认名称), 此 chunk 组包含 ./index.js 模块,随着 parser 处理 ./index.js 内部的 import 时, 新模块就会被添加到此 chunk组中

  • 多入口
module.exports = {   
  entry: {     
    home: './home.js',     
    about: './about.js'   
  } 
}

创建两个名为 home 和 about 的 chunk 组, 每个 chunk 组都包含一个chunk:./home.js 对应 home,./about.js 对应 about

2)Chunk分类

chunk分为initial(初始化) 和non-initial(非初始化)

  • initial(初始化) :是入口起点的 main chunk,此 chunk 包含入口起点指定的所有模块及其同步加载的依赖项

  • non-initial:延迟加载的模块,例如动态导入(dynamic imports) 或者 SplitChunksPlugin

    // initial chunk
    import React from 'react';
    import ReactDOM from 'react-dom';

    // non-initial chunk
    import('./app.jsx').then((App) => {
      ReactDOM.render(<App />, root);
    });

Webpack性能监测

  1. bundle体积监测

webpack-chart

作用:在线分析工具 使用:项目根目录终端运行指令webpack --profile --json 结果:项目根目录下生成一个stats.json文件

缺点:数据直观,但是不够精确,不利于下一步优化

  1. source-map-explorer第三方包

作用:分析内容大小 下载:npm install source-map-explorer -g 运行:npm run source-map-explore 文件名 结果:输出一张标注大小的图

webpack构建速度监测

  1. speed-measure-webpack-plugin插件

npm install speed-measure-webpack-plugin

// webpack.config.js文件
// 导入
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
// 实例化
const smp = new SpeedMeasurePlugin();
// 包裹打包核心代码
module.exports = smp.wrap({
  output: {
    path: `${__dirname}/build`,
    filename: '[name].[contenthash].bundle.js', // 文件名称,hash值不限制长度
    chunkFilename: '[name].[contenthash:8].bundle.js', // 拆分文件名称,8位hash值
  }
  ... ....
});

Webpack常用配置

  1. mode:构建模式

development:开发环境

  • 不会对打包生成的文件进行代码压缩和性能优化
  • 打包速度快,一般在开发阶段使用

production:生产环境,默认模式

  • 会对打包生成的文件进行代码压缩和性能优化
  • 打包速度慢,仅适合在项目发布阶段使用
  1. entry:入口文件,分为单入口和多入口
module.exports = {
    entry: {
        pageOne: './src/pageOne/index.js',
        pageTwo: './src/pageTwo/index.js',
        pageThree: './src/pageThree/index.js',
    }
};
  • dependOn: 当前入口所依赖的入口,必须在该入口加载前加载
  • import: 启动时需加载的模块
  1. module:模块配置,用于指定不同类型的文件如何处理,包括各种加载器、解析器等配置

  2. resolve:解析配置,用于指定模块如何被解析,包括别名、扩展名、模块路径等配置

  3. devServer:开发服务器配置,用于指定如何启动和配置开发服务器,包括端口号、代理、热模块替换等配置

  4. 加载器(Loaders):处理各种类型的文件,可以将文件转换为模块,也可以在转换过程中执行其他任务,例如代码检查、自动添加浏览器前缀等;

Webpack 本身只能处理 JavaScript 模块,但是通过使用不同类型的加载器,可以处理各种类型的文件

  • Loaders的执行顺序是从下到上;
  • Loader 支持链式调用;

常用的加载器如file-loader、url-loader、less-loader、sass-loader

  • html-loader:将html文件转换为字符串,支持压缩、导出、对内容预处理
  • url-loader:将文件转换为base64编码来代替访问地址,可以减少一次网络请求,通过limit参数判断是否超过出配置大小,超过则默认使用file-loader处理
  • file-loader:处理图片、字体等资源
  • css-loader:处理css样式文件
  • style-loader:与css-loader配合使用,将css-loader导出的内容插入到DOM中
  • sass-loader:处理scss样式文件
  • svg-url-loader:将svg问价加载为utf-8编码的data-url字符串
  • svg-sprite-loader:合并多个svg图片为sprite精灵图,并把精灵图插入到HTML内
  1. 插件(Plugins):扩展功能的JavaScript对象,例如代码压缩、代码分离、代码分析、资源管理等
  • HtmlWebpackPlugin:自动生成html文件

  • CleanWebpackPlugin:清除上次构建生成的文件

pluginsloader
plugin是插件,是对webpack本身功能的扩展,是一个扩展器loader用于转化某些类型的模块,是一个转化器
  1. HMR(Hot Module Replacement):热模块替换

即在应用程序运行时,可以动态更新模块的代码,而不需要重新加载整个页面,提高了开发效率和用户体验

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /.vue$/,
        exclude: /node_modules/,
        use: 'vue-loader',
      },
      {
        test: /.css$/,
        use: ['vue-style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
};

Webpack打包环境配置优化

开发环境优化

  1. source map:webpack打包编译后的代码不具备可读性,对于问题定位和分析十分困难,source map可以将打包后的代码映射到源文件,方便问题定位

  1. Webpack-dev-server:基于当前的webpack配置快速启动一个静态服务,仅用于开发环境,页面更改会自动刷新,终端运行npm run serve

  1. 模块热替换HMR:在应用程序运行过程中替换、添加、删除模块,而无需重新加载整个页面
  • 重新加载页面期间会保留应用程序状态;
  • 只更新变更内容,节省时间;
  • 在源代码css/js文件内容修改时,会立刻在浏览器进行更新

调用:手动调用module.hot.accept()或者插件调用react-hot-loader

生产环境优化

  1. 配置文件:区分当前环境
  • 生产环境配置文件:prod.config.js
  • 开发环境配置文件:dev.config.js

  1. 资源压缩:减少文件体积
  • JS压缩

mode配置为production时打包后的代码就是压缩后的代码,webpack5自带terser-webpack-plugin压缩插件,webpack5之前需要下载插件

  • css压缩:css-minimizer-webpack-plugin
  1. 资源体积监控:webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('css-minimizer-webpack-plugin').BundleAnalyzerPlugin;
plugins:[
    new BundleAnalyzerPlugin()
]

Webpack打包优化

  1. Tree-shaking

作用:消除无用的js代码,减小加载文件体积大小,使其整体执行时间更短

如何判断定无用代码?

  • 如果代码不可到达,不会执行;
  • 如果代码执行的结果不会被用到;
  • 如果代码中某个变量只有写,没有读取操作;

那么这些代码会被判定为无用代码。

开启Tree Shaking的前提是什么?

  • 执行的宿主环境宿是Node
  • 使用ES6 Module的模块化(ES6 Module 是静态分析)
  • mode设置为"production"
  • 使用babel-loader 或者 ts-loader 编译代码时,要保留import 和 export

Tree Shaking怎么实现的?

第一步:标记出没有被用过的模块export导出值

第二步:使用Terser删除掉没有被用到的export导出语句

  1. 标记过程实现:
  • 所有 import 标记为 /* harmony import */;
  • 被使用过的 export 标记为/* harm export([type])*/ ,其中[type] 和 webpack 内部相关,可能是 binding,immutable 等等;
  • 未被使用过的 export 标记为 /* unused harmony export [FuncName] */,其中[FuncName] 为export 的方法名称
  1. 删除过程实现:

使用 Terser 把未用到的/* unused harmony export [FuncName] */清除

  1. 副作用无法删除:

有些模块的代码可能会在导入时执行一些副作用,例如改变全局变量、改变导入模块的状态等。这种情况下,即使模块中的部分导出没有被使用,由于其副作用,也不能被 Tree shaking 移除。否则,可能会导致程序运行出错。

import './styles.css'; // 有副作用,改变了全局的样式

webpack4开始,在pacakge.json里面添加一个配置:"sideEffects": false

sideEffects的作用是让 webpack 去除 tree shaking 带来副作用的代码。不管有没有副作用,只要没有被引用,都会被清除。但是会引申出另一个问题,如果配置了,那么很多简单引用都会被忽略,比如引入一个css。所以为什么那么多脚手架都不会去配置这个参数,并不能保证开发者能保证代码都没有副作用。

在package.json中显示的配置,保留副作用。

{
  "name": "your-library",
  "sideEffects": [
    "./src/styles.css"
  ]
}

如果没有副作用,就将值设置为false。

{
  "name": "your-library",
  "sideEffects": false
}
  1. webpack动态打包
  • noparse:直接忽略较大的库,提高webpack的构建速度。被忽略的库不能有import、require、define的引入方式
// webpack.config.js
module.exports = {
  module: {
    noparse: /lodash/rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  }
};
  • DllPlugin:动态库的引用,避免打包时对不变的库重新构建
// webpack.dll.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  plugins: [
    new webpack.DllPlugin({
      name: "",
      path: ""
    }),
  ],
};

3. 代码拆分:将一个bundle文件拆分成若干个小的bundle文件,缩短首屏加载时间

  • 入口拆分:使用entry配置手动分离入口代码

  • 去重分离:使用SplitChunksPlugin或者Entry dependencies去重和分离公共chunk

在compilation.seal函数中对于chunk调用了优化钩子,可以提取公共代码、拆分业务代码

Entry dependencie

SplitChunksPlugin

  1. 动态导入:资源异步加载,当模块数量过多时,把一些当前用不到的模块延迟加载,减少用户初次加载的体积,减少等待时间通过模块的内联函数调用分离代码
// 非动态加载
import {add} from 'math';

// 动态加载
import('matn').then(math => {
    ... ...
})

5. 持久化缓存

  • 每个打包的资源文件都设置一个唯一的hash值
  • 更新文件的同时也会更新文件的hash值
  • 根据hash是否变化去使用浏览器缓存或者重新请求资源
module.exports = {
  output: {
    path: `${__dirname}/build`,
    filename: '[name].[contenthash].bundle.js', // 文件名称,hash值不限制长度
    chunkFilename: '[name].[contenthash:8].bundle.js', // 拆分文件名称,8位hash值
  }
};

hash:每次运行build打包文件时,css与对应的js文件的hash值相同

contenthash:hash值与文件具体内容有关,全局唯一性,只修改css,jshash值不会变化

9. Babel解析优化

Babel 是一个JavaScript编译器,主要用于将ECMAScript 2015+语法的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器中

babel的工作流程

分为三个阶段,解析(parse),转换(transform),生成(generate)

解析:通过 @babel/parser 把源代码字符串转成抽象语法树(AST),解析过程主要是词法分析和语法分析 转换:通过@babel/traverse遍历抽象语法树(AST),并调用Babel配置文件中的插件,对抽象语法树(AST)进行增删改 生成:通过@babel/generator把转换后的抽象语法书(AST)生成目标代码

Babel本身不具备转化功能,而是将转化功能分解到插件中,如果不配置任何插件,经过babel的代码和输入是相同的

  1. 语法插件:仅允许 babel 解析语法,不做转换操作,babel-plugin-syntax-trailing-function-commas
  2. 转译插件:将源代码进行转译输出,babel-plugin-transform-es2015-arrow-functions
箭头函数 (a) => a 转化为 
function (a) {
  return a
}

babel配置项

  1. babel-core:babel的核心,根据配置文件转换代码,配置文件一般是.babelrc(静态文件)或 babel.config.js(可编程)
  • 加载和处理配置(config)
  • 加载插件
  • 调用 Parser 进行语法解析,生成语法树
  • 调用 Traverser 遍历书法树,并使用访问者模式应用'插件'对语法树进行转换
  • 生成代码,包括SourceMap转换和源代码生成
  1. babel-cli:命令行工具,能够在命令行中使用 babel 来编译文件

  2. babel-node:在node环境中,直接运行es2015的代码,不需要额外代码转换

  3. babel-polyfill:既可以转换 js 语法又可以转换新的 API,如 Generator,Maps等全局对象、一些定义在全局对象上的方法(Object.assign)

  4. babel-loader:webpack 打包的时候,将ES6的代码转换成ES5版本的代码

10. 资源加载优化

1)资源压缩

  • 在线工具压缩
  • npm工具压缩
  • Webpack构建压缩

2)资源合并

  • 若干个小文件
  • 小文件无冲突,服务相同的模块

合并后一个更改就请求全部,与缓存概念违背 合并后多大不利于骨架屏的使用,白屏时间可能会延长

3)资源按需加载

基于路由按需加载是目前来说比较主流的方案,可以使用Reloadable高级组件实现基于路由的按需加载

下载:npm install @loadable/component -g 导入:import loadable form '@loadable/component';

const LoadableLogin = loadable(() => import('./login.tsx'),{
    fallback: '<div>加载中......</div>'
})

4)资源加载顺序优化

  • preload:提前加载一个较晚出现的资源文件
  • prefetch:提前加载后续路由需要的资源文件,优先级低

11. 资源传输优化

在nginx代理或者服务器上进行相关配置,使文件在传输过程中的大小进行压缩,提高传输速度

  • 启用压缩Gzip
  • 启用Keep Alive(nginx默认开启)

keepalive_timeout:超时时间 keepalive_requests:利用持久的TCP链接可以发送的请求上限

12. 利用缓存技术

1)浏览器缓存

  1. Cache-Control/Expirse强缓存
  2. Last-Modified + If-Modified-Since协商缓存
  3. Etag + If-Node-Match协商缓存

2)Service Workers

在浏览器端创建了一个中间层,用于缓存资源。延长了首屏加载时间,并且兼容性不高,必须使用https协议。但可以加速重复访问时间,支持离线访问

13. 首屏渲染优化

  • First ContentFul Paint(FCP) :第一个有意义内容的绘制,用户等待白屏时间
  • Largest ContentFul Paint(LCP) :最大内容的绘制,用户可以对网站有大致了解
  • Time to Interactive(TTI):用户可以进行交互

1)服务端渲染SSR:加快首屏渲染、引擎优化,一般用于大型、动态页面。Next.js框架已实现SSR npm install next react react-dom

2)预渲染页面:React-snap通过牺牲网络请求耗时(TTFB)来缩短首屏时间

let root = document.getElementById("main");
if(root.hasChildNodes()){
    ReactDOM.hydrate(<App/>,root);
}else{
    ReactDOM.render(<App/>,root);
}

3)骨架组件:减少布局移动问题,减少回流出现,实际应用中例如骨架屏、占位loading符。常用插件react-placeplaceholder

<ReactPlaceholder>     
  //会发生变化的元素 
</ReactPlaceholder>

4)Windowing窗口化:提高列表渲染性能react-window

5)虚拟列表

import {FixedSizeList} from 'react-window';

const lists = (
    <FixedSizeList height='' width='' itemSize='' itemCount='1000' className=''>
        <Row />
    </FixedSizeList>
)

只渲染可见元素

14. JS占用内存优化

1)垃圾回收机制

程序的运行需要内存,对于持续运行的服务进程,及时去释放无用对象的内存,即为垃圾回收。JavaScript 内存管理中有一个概念叫做可达性,就是那些以某种方式可访问或者说可用的值,它们存储在内存中,我们需要清理的就是那些不可达的值所占用的内存。

JavaScript 垃圾回收机制的原理即定期找出那些不再用到的变量,释放其占用的内存

局部变量内存回收:函数执行完释放(没有闭包引用) 全局变量内存回收:浏览器页面卸载时释放

2)回收方式

标记清除:垃圾收集器在运行时会给内存中的所有变量都加上一个标记

  • 假设内存中所有对象都是垃圾,全标记为0;
  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1;
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间;
  • 把所有内存中对象标记修改为0,等待下一轮垃圾回收;

特点:内存碎片化(空闲内存块是不连续的)、分配速度慢(因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作)

引用计数:把对象是否不再需要简化定义为对象有没有其他对象引用,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,但因为问题很多,目前很少使用这种算法了

  • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1;
  • 如果同一个值又被赋给另一个变量,那么引用数加 1;
  • 如果该变量的值被其他的值覆盖了,则引用次数减 1;
  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

特点:立即回收垃圾、但是需要一个计数器(内存占用过大),且无法解决循环引用的问题

分代式回收:将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收 分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查,新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率

3)内存泄漏优化

当不再用到的对象内存没有及时被回收,这种场景称之为内存泄漏

  1. 不正当的闭包:可以在函数调用后,把外部的引用关系置空
  2. 隐式全局变量:可以使用严格模式、通过 lint 检查、即使将全局变量赋值为null
  3. 游离DOM引用:可以缓存引用的变量置空
  4. 定时器:使用clearInterval(清除setInterval )、clearTimeout(清除setTiemout)、cancelAnimationFrame(清除requestAnimationFrame)
  5. 事件监听器addEventListener:使用removeEventListener清除
  6. Map、Set对象:Map强引用改为WeakMap弱引用(只有键是弱引用),Set强引用改为WeakSet弱引用(只有键是弱引用),并在使用完成后置空
  7. Console:清除项目代码内的console

4)内存膨胀优化

内存膨胀通常表现为内存在某一段时间内快速增长,然后达到一个平稳的峰值继续运行;

内存膨胀和内存泄漏有一些差异,内存膨胀主要表现在对内存管理的不科学,额外使用过多的内存,有可能是没有充分地利用好缓存,也有可能加载了一些不必要的资源

解决:合理规划项目,利用缓存、减少服务器请求等从而减轻项目中不必要的内存占用

5)频繁的垃圾回收

频繁使用大的临时变量,导致了新生代空间很快被装满,从而频繁触发垃圾回收

解决:将这些临时变量设置为全局变量

15. Web攻击优化

1)XSS攻击

跨站脚本攻击(Cross-Site Scripting,简称XSS)是一种攻击方式,攻击者通过在合法网站中嵌入恶意脚本,使其在用户浏览器中执行,从而窃取用户信息、劫持会话等

常见攻击

  • 绕过XSS-Filter,利用<>标签注入Html或JavaScript代码
  • 利用HTML标签的属性值进行xss攻击,如:<img src="转存失败,建议直接上传图片文件 “javascript:alert(‘xss’)”/" alt="转存失败,建议直接上传图片文件">(由于浏览器对于Javascript伪协议兼容性不高,所以此类攻击有局限性)
  • 利用空格、回车、Tab键绕过过滤
  • 利用事件执行跨站脚本,如:<img src="转存失败,建议直接上传图片文件 “#”" onerror="“alert(1)”/" alt="转存失败,建议直接上传图片文件">
  • 利用CSS跨站,如:Body {backgrund-image: url(“javascript:alert(‘xss’)”)}
  • 扰乱过滤规则,如:<img src=“javaSCript: alert(/xss/);”/>
  • 利用字符编码不仅能让XSS代码绕过服务端的过滤,还能隐藏Shellcode
  • 拆分跨站法,将xss攻击的代码拆分开来,适用于应用程序没有过滤 XSS关键字符(如<、>)却对输入字符长度有限制的情况下;

预防攻击方式

  • 输入验证和过滤:对用户输入利用正则表达式、白名单过滤等方法进行验证和过滤,限制特殊字符和标签
  • 输出编码:将用户输出进行编码后再显示在页面上,如HTML实体编码、URL编码和JavaScript编码
  • Content Security Policy(CSP):限制页面中加载的资源和执行的脚本
  • 安全的Cookie设置:将敏感Cookie标记为HttpOnly,防止脚本访问
  • 使用最新版本的JavaScript库和框架,及时修复安全漏洞

2)CSRF攻击

跨站请求伪造(Cross-Site Request Forgery,简称CSRF)是一种攻击方式,攻击者通过伪造用户已认证的请求,使用户在不知情的情况下执行恶意操作

预防攻击方式

  • 使用CSRF令牌:为每个用户生成唯一的CSRF令牌,并包含在每个请求中,服务器验证令牌的有效性
  • 同源检测:检查请求来源是否与预期的域名一致,不匹配则拒绝请求
  • 验证HTTP 请求头:检查请求头中的Referer字段,确保请求来源于预期的页面
  • 避免使用GET请求执行敏感操作,使用POST请求
  • 添加验证码:要求用户输入验证码以确认身份

延伸 输入URL后的流程⭐️⭐️⭐️⭐️⭐️

1. 底层流程步骤

第一步:浏览器查找当前URL是否存在缓存,并比较缓存是否过期。(浏览器缓存 > 系统缓存 > 路由器缓存 > hosts文件)

第二步:DNS解析域名获取服务器的ip地址

第三步:浏览器与服务器建立TCP链接(https的SSL层)

第四步:浏览器发送请求到服务器

第五步:服务器收到请求后,解析请求并作出响应并返回给浏览器

第六步:浏览器解析文件并绘制图像

第七步:TCP四次挥手,服务器与浏览器断开连接

2. 浏览器主要功能

浏览器的主要功能:将用户输入的url转变成可视化的图像

  1. 网络下载:浏览器通过网络模块来下载各种资源文件
  2. 资源管理:从网络下载或者本地获取到的资源进行管理,如浏览器缓存
  3. 网页浏览:浏览器的核心功能,将资源转变为可视化的结果
  4. 多页面管理
  5. 插件与管理
  6. 账户和同步
  7. 安全机制
  8. 开发者工具

浏览器内核(渲染引擎):把请求的资源变成可视化的图像

  1. HTML解析器:解析HTML文本构建DOM树
  2. CSS解析器:解析CSS样式,计算DOM树中的各个元素对象的样式信息
  3. JavaScript引擎:解释JavaScript代码,通过DOM、CSS接口修改网页内容和样式信息改变渲染的结果
  4. 布局layout:也称为重排,构建内部表示模型,将元素对象同样式结合并计算节点的大小、位置等布局信息
  5. 绘图模块paint:使用图形库将网页的节点绘制成图像结果

3. 进程和线程

进程:程序的每一次执行都有一个独有的内存空间,是操作系统执行的基本单元

线程:是进程内的一个独立执行单元,是CPU调度的最小单元

  • 一个进程中至少有一个运行的线程(主线程),主线程在进程启动后自动创建
  • 一个进程可以同时运行多个线程,一般程序是多线程运行的,如使用软件,软件就是一个进程,软件里听歌、点赞评论就是多个线程操作
  • 一个进程中的数据可以供其中的多个线程直接共享,但是进程与进程之间的数据不能共享的
  • JS引擎是单线程运行的

4. 浏览器渲染流程

  1. 浏览器从上到下解析文档
  2. 用HTML解析器解析HTML标记,生成对应的token(标签文本的序列化)
  3. 根据tokens及关联关系构建DOM树
  4. 用CSS解析器解析style/link标记,构建CSS样式树(从右到左)
  5. 用JavaScript引擎处理script标记
  6. 将DOM树与CSS样式树合并成一个渲染树
  7. 计算渲染树中每个节点的几何信息(此过程需要依赖GPU)
  8. 将各个节点绘制在屏幕上(分图层绘制)

5. CSS阻塞及优化

  1. 内联样式:由HTML解析器进行解析,不会阻塞DOM解析及浏览器渲染,可能会产生“闪屏现象”
  2. 外部样式:由CSS解析器进行解析,不阻塞DOM的解析,会阻塞后面的js语句执行及浏览器渲染

css 阻塞优化

  • 使用CDN节点进行外部资源加速
  • 压缩CSS文件
  • 优化CSS代码,选择器嵌套层级不要过深

HTML和CSS是并行解析的,所以CSS不会阻塞HTML解析,但是会阻塞整体页面的渲染(DOM树和CSS样式树合并时间延迟了)

CSS解析顺序

CSS解析为从最底层开始,从右到左解析,即孙节点1 —> 孙节点2 —>孙节点3,而不是爷节点—>父节点—>孙节点1

6. JS阻塞及优化

  1. JS会阻塞后续DOM的解析:js可以操作DOM(避免做无用功)
  2. JS会阻塞页面渲染:js可以操作CSS样式(避免做无用功)
  3. JS会阻塞后续js的执行:维护js间的依赖关系

css阻塞及js阻塞都不会阻塞浏览器加载外部资源(图片、视频、样式、脚本等),因为浏览器的工作模式为优先发送请求,拿到资源后由浏览器协调何时使用资源

7. 重绘回流

  1. 重绘:元素外观的改变所触发的浏览器行为,如outline、背景色等属性,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观
  2. 回流:渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息,计算这些值的过程称为布局或重排

‘重绘’不一定需要‘重排’,‘重排’大多数情况下会导致‘重绘’

常见重绘回流行为

  1. 增加、删除、修改 DOM 结点:回流+重绘
  2. 移动 DOM 位置:回流+重绘
  3. 修改 CSS 样式:重绘
  4. Resize 窗口(移动端的缩放没有影响布局视口):回流+重绘
  5. 修改网页的默认字体:回流+重绘
  6. 获取DOM的height或者width:如clientWidth、offsetWidth、scrollWidth、getComputedStyle()、getBoundingClientRect()等

优化回流重绘

  1. 元素位置移动变换时尽量使用CSS3的transform代替top,left等操作
  2. 不要使用table布局
  3. 将多次改变样式的操作合并成一次操作
  4. 利用文档碎片(documentFragment),vue使用了该方式提升性能
  5. 动画实现过程中,启用GPU硬件加速
  6. 为动画元素新建图层transform:tranlateZ(0),提高动画元素的z-index
  7. 编写动画时,尽量使用requestAnimationFrame

documentFragment是一个保存多个element的容器对象(保存在内存)当更新其中的一个或者多个element时,页面不会更新,只有当documentFragment容器中保存的所有element更新后再将其插入到页面中才能更新页面