Web 性能优化

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 如何显示 => 虚拟滚动列表
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容