美文网首页
Web 性能优化

Web 性能优化

作者: bowen_wu | 来源:发表于2022-10-06 00:44 被阅读0次

    Web 性能优化

    DNS

    • DNS => Domain Name System
    • 域名需要转化成 IP => 浏览器(缓存) -> 操作系统(hosts) -> 运营商

    TCP 连接

    • TCP => Transmission Control Protocol => 传输控制协议
    • 三次握手 => 确保客户端和服务端都可以收发消息
    TCP 三次握手和四次挥手

    HTTP 请求

    • HTTP => Hypertext Transfer Protocol => 超文本传输协议

    Request

    动词 URL HTTP/1.1
    Accept: text/html
    Host: baidu.com
    Connection: keep-alive
    Content-Type: application/json
    ...
    
    {"id": "1"}
    

    Response

    HTTP/1.1 200 OK
    Content-Type: text/html
    Set-Cookie: session_id=xxx; Cache-Control: max-age=3600
    Connection: keep-alive
    ...
    
    {"info", "this is response"}
    

    HTML 解析过程

    • 解析 HTML 会构建 DOM 树
    • 解析 CSS 会构建 CSS 树
    • JS 的下载和执行会阻塞 HTML 的解析 => 下载和执行 JS 文件会修改 DOM 树
      1. 执行 JS 可以会修改 DOM 树
      2. 解析是一行一行的,只有解析到 script 行,才能去下载
    • CSS 的下载和解析会阻塞 JS 的执行,JS 的执行需要等到 CSS 下载和解析结束 => JS 需要读取 CSS 结果
    HTML 解析过程

    async/defer

    • <script defer/async ...
    • defer => 不会阻塞 HTML 的解析,保证 JS 的执行是在 HTML 解析之后,DOM ready 事件之前。执行完所有 JS 文件会触发 DOM ready 事件。多个 JS 文件下载之后按书写顺序执行
    • async => 不会阻塞 HTML 的解析,DOM ready 事件是在 HTML 解析之后触发,不确定执行 JS 文件和 DOM ready 事件的先后关系,多个 JS 文件下载完之后立即执行
    script parse

    页面渲染

    • DOM 树 + CSS 树 => 渲染树
    • DOM 树 + CSS 树 -> 渲染树 -> Layout -> Paint -> Composite
    • Layout => 布局 => 大小、尺寸 => reflow => 重排
    • Paint => 绘制 => 颜色、阴影 => repaint => 重绘
    • Composite => 合成 => 层次 => transform 只会触发 Composite

    Web 性能优化

    • 指标 => DOM ready 事件发生 => DOMContentLoaded
    • Dom ready 事件之前阶段
      1. DNS 解析 => DNS 预解析
      2. TCP 连接 => 连接复用(HTTP/1.1 默认) + 并行连接(并行发送请求)
      3. HTTP/2 => 多路复用(HTTP/2 默认)
      4. HTTP/1.1 => 资源合并 + 内联 + 压缩 + 精简 + cookie-free + CDN + 缓存 + 内容协商
      5. CSS 优化 + JS 优化
      6. 代码优化 => 位置 + 拆分 + 动态导入 + 懒加载 + 预加载

    DNS

    • DNS 预解析
    <script src="http://a.com/1.js"></script>
    <script src="http://b.com/2.js"></script>
    

    需要解析 a.com 和 b.com,并且 b.com 必须要等到 1.js 下载并执行完之后才能解析

    // 在 index.html 中的 <head> 里面写
    <link rel="dns-prefetch" href="https://a.com/"/>
    <link rel="dns-prefetch" href="https://b.com/"/>
    
    // 在 index.html 的响应头中写
    Link: <https://a.com/>; rel=dns-prefetch
    

    TCP 连接

    连接复用

    • Connection: keep-alive => HTTP 的 keep-alive 实现 TCP 连接的复用
    • 请求头中添加 Connection: keep-alive,并且在响应头中也添加 Connection: keep-alive
    • Keep-Alive: timeout = 5, max = 100 => 两次请求的时间间隔小于多少认为可以复用同一个 TCP 连接 => 请求头和响应头都可设置,最终决定于响应头
    • HTTP/1.1 及以上 Connection: keep-alive 是默认添加的
    • 串行的

    并发连接

    • 连接复用是串行的,并发连接是并行的
    • 并行连接有最大数量 => 不同的浏览器上限不同 => 大概是 4 - 12 个,大部分是 6 个
    • case: 将请求拆分 => case:不要一个 css,要多个 css,可以并行下载
    • case: 发送多个 ajax => 并行发送

    HTTP 管道化 HTTP pipelining

    • HTTP/1.1 有 bug

    HTTP/2

    • HTTP/1.1 基于字符串 => HTTP/2 基于帧 Frame
    • HTTP/2 每一帧的组成
      1. 9 byte => Length + Type + Flags + StreamId => 用于标记
      2. 最大 16 M => Payload => 数据
    • 请求头和响应头会被发送方压缩后,分成几个连续的 Frame 传输,接收方拼合这些 Frame 后,解压缩即可得到真正地请求头和响应头
    • 引入流 Stream的概念,一个 Stream 由双向传输的连续且有序的 Frame 组成,一个 TCP连接可以同时包含多个 Stream,一个 Stream 只用于一次请求和一次相应。Stream 之间不会相互影响
    • 头部字段改为小写,不允许出现大写
    • 引入了伪头部字段的概念,出现在头部字段前面,必须以冒号开头
    • 服务端可以先发响应,客户端拿到响应结果后可以保存,之后就不需要在发对应的请求了
    // get => : 伪头 => 表明 HTTP/1.1 的第一部分
    //     => header 都是小写
    //  伪头 + header -> 可能是一个 Frame
    :method: GET
    :scheme: https
    :path: /zh-CN/docs/Web/CSS/Cascade
    accept: text/html
    cookie: xxx
    cache-control: no-cache
    
    // POST 有多个 Frame
    

    多路复用

    • 在一个 TCP 连接中可以同时进行多个请求与响应,每一个请求+响应都在同一个 Stream 上,由于有标记的 9 个字节,所以响应能够和请求对应上
    • 一个 Stream 只能对应一个请求 + 响应
    • 有了多路复用就不需要并行连接了

    资源合并

    • Icon Font => 将图标变成字体文件
    • SVG Symbols => 使用 SVG 文件代替图标,case:一个 svg 里面有很多图标,使用 <use xlink:href="#user">
      1. 支持渐变 => Icon Font 不支持渐变
      2. SVG 编辑较 Icon Font 简单

    资源内联

    • 资源内联可以减少一些 TCP 连接
    • 但是如果文件过大,传输时间过长,不如并行
    • 小图片 => data URL => date url 可以通过 webpack 的 url-loader 编译
    • 小 CSS 文件 => <style>代码</style> => 通过 webpack 插件可以实现
    • 小 JS 文件 => <script>代码</script> => 通过 webpack 插件可以实现

    资源压缩

    代码精简

    • HTML => 删空格、删闭合
    • CSS => 删未用
    • JS => 改名、tree shaking
    • SVG => 删除无用标签、属性
    • 图片 => 减少体积(有损/无损)

    减少 Cookie 体积

    • 最大 4kb
    • 同一个域名每次请求都带着
    • 启用新域名 => 新域名不会带着其他域名的 Cookie => 实现了连接并发和清 Cookie => cookie-free

    CDN

    • CDN => 内容分发网络 => 负载均衡
    • 域名传输到 DNS,DNS 会给一个最近的 IP
    • 优点
      1. cookie free
      2. 并行请求/多路复用
      3. 下载速度快
    • 缺点
      1. 跨域 => 设置 CORS
      2. 可控性差 => CDN 如果挂了,依赖 CDN
      3. 部署相对复杂

    缓存 & 内容协商

    • 强缓存 & 弱缓存
    • Cache-Control: max-age=3600; => 缓存1小时,如果再次请求,则不会发送请求,而是直接返回缓存内容 => memory cache | disk cache
    • 服务器主动更新缓存 => 首页不会缓存,首页引入新的文件 => 生成新的 hash
    • 内容协商 => 缓存过期后能重用吗?=> 缓存过期后再次请求就是协商请求(带上 ETag) => 服务端会对比 ETag => If-None-Match: <Etag Value>
      1. 304 + 空响应 => Not Modify => 使用之前的缓存
      2. 200 + 新文件 => 删除或覆盖之前的缓存
    缓存 内容协商
    HTTP/1.1 Cache-Control: max-age=3600; ETag: XXX 请求头 => If-None-Match: XXX
    响应 => 304 + 空 | 200 + 新内容
    HTTP/1.0 Expires: 时间点A; Last-Modified: 时间点B 请求头 => If-Modified-Since: 时间点B
    响应 => 304 + 空 | 200 + 新内容

    Cache-Control

    • public/private => 中间设备是否可以缓存
    • max-age => 缓存时间
    • must-revalidate => 必须重新校验

    禁用缓存

    • 没有 Cache-Control 的情况下,浏览器也会缓存 => case: GET + (200 | 301)
    • 服务端 => response header =>
      1. Cache-Control: max-age=0,must-revalidate === Cache-Control: no-cache => 不缓存,但是可以协商
      2. Cache-Control: no-store => 不缓存、不协商
    • 浏览器端 =>
      1. url 添加随机数 => get /user?_=随机数
      2. request header => Cache-Control: no-cache, no-store, max-age=0

    DNS 缓存

    1. 操作系统缓存 IP
    2. 浏览器缓存 IP

    代码优化

    代码位置

    • css 文件放在前面 => 有利于用户看到样式,否则可能白屏或闪烁(firefox + css 在 body 内) => 下载过慢也可导致白屏或闪烁(firefox + css 在 body 内)
      1. 不阻塞 HTML 解析,尽早下载
      2. 防止被外部 JS 阻塞
    • JS 文件放在后面
      1. 可直接访问 DOM,无需关注 DOM ready 事件
      2. 避免阻塞 HTML 解析

    代码拆分

    • 根据变动频率进行分层。case:webpack 最终打包成 main-xxx.js(1MB),之后更改了一个文案,重新打包 main-yyy.js(1MB) => 用户需要重新下载
    • JS 分层
      1. runtime-xxx.js => webpack 升级用到的
      2. vendor-xxx.js => 第三方库 Vue | React
      3. common-xxx.js => 公司级别的基础库
      4. <page>-index-xxx.js => 每个页面 => 上述只会更改这一个文件
      // webpack.config.js
      module.exports = {
        // <page>-index-xxx.js
        entry: {
          app: './src/page/app.js',
          main: './src/page/main.js',
          admin: './src/page/admin.js'
        },
        plugins: [
          new HtmlWebpackPlugin({
            filename: 'app.html',
            chunks: ['app']
          }),
          new HtmlWebpackPlugin({
            filename: 'main.html',
            chunks: ['main']
          }),
          new HtmlWebpackPlugin({
            filename: 'admin.html',
            chunks: ['admin']
          })
        ],
        optimization: {
          runtimeChunk: 'single', // runtime-xxx.js
          splitChunks: {
            cacheGroups: {
              vendor: { // vendor-xxx.js
                priority: 10,
                minSize: 0, // 如果不写 0,由于 React 文件尺寸太小,会直接跳过
                test: /[\\/]node_modules[\\/]/, // 为了匹配 /node_modules/ 或 \node_modules\
                name: 'vendors', // 文件名
                chunks: 'all' // all 表示同步加载和异步加载,async 表示异步加载,initial 表示同步加载
                // 这三行的整体意思就是把两种加载方式的来自 node_modules 目录的文件打包为 vendors.xxx.js
              },
              common: { // common-xxx.js
                priority: 5,
                minSize: 0,
                minChunks: 2,
                name: 'common',
                chunks: 'all'
              }
            }
          }
        }
      }
      
    • CSS 分层
      1. reset/normalize-xxx.css => 基础
      2. vendor-xxx.css => 第三方库 antd
      3. common-xxx.css => 公司级别
      4. <page>-index-xxx.css => 每个页面

    JS 动态导入

    • 有些 JS 文件用到的时候在下载
    const array = [1, 2, 3];
    import("lodash").then(_ => {
      const clone = _.cloneDeep(array);
    });
    
    import React, {Suspense, lazy} from 'react';
    import {BrowserRouter as Router, Route, Switch} from 'react-route-dom';
    
    const Home = lazy(() => import('./pages/Home'));
    const About = lazy(() => import('./pages/About'));
    
    const App = () => {
      <Router>
        <Suspense fallback={LoadingComponent}>
          <Switch>
            <Route exact path="/" component={Home}/>
            <Route page="/about" component={About}/>
          </Switch>
        </Suspense>
      </Router>
    }
    

    图片懒加载(Lazy Loading) 和预加载

    • 对于多屏图片,第一次请求只请求第一屏的图片,当滚动到第二屏的时候再请求第二屏的图片
    • 懒加载太慢了,第一次请求第一屏和第二屏的图片,滚动到第二屏的时候,请求第三屏的图片
    <img src='product.jpg'>
    
    // 修改为 => placeholder.png 很小
    <img src='placeholder.png' data-src='product.jpg'>
    
    // 监听滚动事件 => 对于每一个下一屏的图片
    new Image().src = img.dataset.src
    // 监听 new Image 的 onload 事件,之后将 img 的 src 替换
    img.src = img.dataset.src
    

    CSS 优化

    1. 删除无用 css
    2. 使用更高效的选择器
    3. 减少重排 => reflow => 将 .left 动画更改为 transform 动画
    4. 不要使用 @import url.css => 不能并行
    5. 启用 GPU 硬件加速 => transform: translate3d(0, 0, 0)
    6. 使用缩写 =>
      1. FFFFFF -> #FFF

      2. 0.1 => .1
      3. 0px => 0

    JS 优化

    1. 尽量不用全局变量 => 全局变量过多会使变量查找变慢
    2. 尽量少操作 DOM => 可以使用 Fragment 一次性插入多个 DOM 节点
    3. 尽量少触发重排 => 可以使用节流和防抖来降低i重排频率
    4. 尽量少用闭包,避免内存泄漏 => IE 浏览器的 Bug
    5. 1W个 list 如何显示 => 虚拟滚动列表

    相关文章

      网友评论

          本文标题:Web 性能优化

          本文链接:https://www.haomeiwen.com/subject/byimartx.html