来呀来呀,丸子带您学浏览器渲染原理和性能优化

一、进程和线程

  1. 进程与线程
  • 进程:

是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位

  • 线程:

是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

总的来说:

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

  1. 浏览器中得5个进程

image.png

浏览器加载资源机制

浏览器会开辟一个 GUI 渲染线程,自上而下执行代码,专门用于渲染渲染页面的线程。

  • 浏览器进程:负责页面显示、用户交互、子进程管理、提供存储等
  • 渲染进程:每个页面都有单独的渲染进程,核心用于渲染页面
  • 网络进程:主要处理网络资源加载(HTML、CSS、JS等)
  • GPU进程:3d绘制,提高性能
  • 插件进程:chrome中安装的一些插件

二、从输入URL到浏览器显示页面发生了什么

用户输入的关键字还是URL?如果是关键字则使用默认引擎生产URL

  1. 浏览器进程的相互调用
  • 在浏览器中输入url地址开始导航。准备渲染进程
  • 在网络进程中发送请求,将响应后的结果交给渲染进程先处理
  • 解析页面,加载页面中所需资源
  • 渲染完毕,展示结果 我们开始细化每一步流程,并且从流程中提取我们可以优化的优点
  1. URL请求过程

网络七层模型:物,数,网,传,会,表,应

(物,数),网(ip),传(tcp安全可靠,udp会丢包,但是速度较快),(会,表,应)

  • 浏览器先去查找当前的URL是否存在缓存,检测缓存是否过期。直接返回缓存中的内容
  • 看域名是否被解析过,没有解析进行DNS解析 将域名解析成IP地址(DNS基于UDP) ip + 端号+ host
  • 请求是HTTPS ,进行SSL协商
  • 如果利用ip地址来进行寻址,排队等候,最多能发送6个http请求
  • 排队后服务器创建tcp链接 用于传输(三次握手)
  • 利用tcp协议将大文件拆分成数据包进行传输(拆分成数据包 有序) 可靠、有序、服务器会按照顺序来重排数据包
  • 发送http请求(请求行,请求头,请求体)
  • HTTP1.1中支持keep-alive属性,TCP链接不会立即关闭,后续请求可以省去建立链接是啊金
  • 服务器收到数据后响应结果(响应行 响应头 响应体)
  • 服务器返回301 302 ,浏览器会进行重定向操作(重新进行缓存)
  • 服务器304去查询浏览器缓存进行返回(服务端可以设置强制缓存)
  • 通过network Timing观察请求发出流程

image.png

Queuing 请求发送前会根据优先级进行排队,同事每个域名最多处理6个TCP链接,超过的也会进行排队,并且分配磁盘空间时也会消耗一定时间
Stalled 请求发出前的等待时间(处理代理,链接复用)
DNS lookup 查找DNS的时间
initial Connection 建立TCP链接时间
SSL SSL握手时间(SSL协商)
Request Sent 请求发送时间(可忽略)
Waiting(TTFB) 等待响应的时间,等待返回首个字符的时间
Content Dowloaded 用于响应下载的时间

performance.getEntrirs()用于获取页面所有资源的performance timing情况

蓝色:DOMContentLoaded:DOM构建完成的时间

红色:Load;浏览器所有资源在加载完毕

  1. HTTP发展历史
  • http 0.9 负责传输html 最早的时候没有请求头和响应头
  • http 1.0 提供了http得header 根据Header得不同来处理不同地资源
  • http 1.1 默认开启了keep-alive链接复用 管线化 服务器处理多个请求(对队阻塞问题)
  • http 2.0 用同一个tcp链接来发送 数据 一个域名一个tcp(多路复用) 头部压缩 服务器可以推送数据给客户端
  • http 3.0 解决了tcp得队头阻塞问题QUIC协议 采用了udp
  1. 渲染流程

image.png

  1. 浏览器无法直接使用html,需要将HTML转化成DOM树(document)
  2. 浏览器无法直接解析纯文本得CSS样式,需要对CSS进行解析,解析成styleSheets。CSSOM(document.styleSeets)
  3. 计算出DOM树中每个结点的具体样式(Attachment)
  4. 创建渲染(布局)树,将DOM树中可见节点,添加到布局树中。并计算节点渲染到页面得坐标位置。(layout)
  5. 通过布局树,进行分层(根据定位属性、透明属性、transform、clip属性等)生产图层树
  6. 将不同图层进行绘制,转交给合成线程处理,最终产生页面,并显示到浏览器上(Painting,Display)查看layer并对图层进行绘制的列表

图层: image.png

三、模拟请求->渲染流程

请求报文格式

  • 起始行:[ 方法 ] [空格 ] [请求URL] [http版本] [换行符]
  • 首部:[首部名称] [:] [空格] [首部内容] [ 换行符 ]
  • 首部结束:[换行符]
  • 实体

响应报文格式

  • 起始行:[HTTP 版本] [空格] [状态码] [空格] [原因短语] [换行符]
  • 首部:[首部名称] [:] 空格 [换行符]
  • 首部结束: [换行符]
  • 实体
  1. 基于 TCP 发送 HTTP 请求
const net = require("net");
class HTTPRequest {
  constructor(options) {
    this.method = options.method || "GET";
    this.host = options.host || "127.0.0.1";
    this.port = options.port || 80;
    this.path = options.path || "/";
    this.headers = options.headers || {};
  }
  send(body) {
    return new Promise((resolve, reject) => {
      body = Object.keys(body)
        .map((key) => `${key}=${encodeURIComponent(body[key])}`)
        .join("&");
      if (body) {
        this.headers["Content-Length"] = body.length;
      }
​
      const socket = net.createConnection(
        {
          host: this.host,
          port: this.port,
        },
        () => {
          const rows = [];
          rows.push(`${this.method} ${this.path} HTTP/1.1`);
          Object.keys(this.headers).forEach((key) => {
            rows.push(`${key}: ${this.headers[key]}`);
          });
          let request = rows.join("\r\n") + "\r\n\r\n" + body;
          socket.write(request);
        }
      );
​
      socket.on("data", function(data) {
        // data 为发送请求后返回的结果
      });
    });
  }
}
async function request() {
  const request = new HTTPRequest({
    method: "POST",
    host: "127.0.0.1",
    port: 3000,
    path: "/",
    headers: {
      name: "zhufeng",
      age: 11,
    },
  });
  let { responseLine, headers, body } = await request.send({ address: "北京" });
}
​
request();
复制代码
  1. 解析响应结果
const parser = new HTTPParser();
socket.on("data", function(data) {
  // data 为发送请求后返回的结果
  parser.parse(data);
  if (parser.result) {
    resolve(parser.result);
  }
});
复制代码
  1. 解析 HTML
let stack = [{ type: "document", children: [] }];
const parser = new htmlparser2.Parser({
  onopentag(name, attributes) {
    let parent = stack[stack.length - 1];
    let element = {
      tagName: name,
      type: "element",
      children: [],
      attributes,
      parent,
    };
    parent.children.push(element);
    element.parent = parent;
    stack.push(element);
  },
  ontext(text) {
    let parent = stack[stack.length - 1];
    let textNode = {
      type: "text",
      text,
    };
    parent.children.push(textNode);
  },
  onclosetag(tagname) {
    stack.pop();
  },
});
parser.end(body);
复制代码
  1. 解析 CSS
const cssRules = [];
const css = require("css");
function parserCss(text) {
  const ast = css.parse(text);
  cssRules.push(...ast.stylesheet.rules);
}
const parser = new htmlparser2.Parser({
  onclosetag(tagname) {
    let parent = stack[stack.length - 1];
    if (tagname == "style") {
      parserCss(parent.children[0].text);
    }
    stack.pop();
  },
});
复制代码
  1. 计算样式
function computedCss(element) {
  let attrs = element.attributes; // 获取元素属性
  element.computedStyle = {}; // 计算样式
  Object.entries(attrs).forEach(([key, value]) => {
    cssRules.forEach((rule) => {
      let selector = rule.selectors[0];
      if (
        (selector == "#" + value && key == "id") ||
        (selector == "." + value && key == "class")
      ) {
        rule.declarations.forEach(({ property, value }) => {
          element.computedStyle[property] = value;
        });
      }
    });
  });
}
复制代码
  1. 布局绘制
function layout(element) {
  // 计算位置 -> 绘制
  if (Object.keys(element.computedStyle).length != 0) {
    let { background, width, height, top, left } = element.computedStyle;
    let code = `
            let canvas = document.getElementById('canvas');
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight ;
            let context = canvas.getContext("2d")
            context.fillStyle = "${background}";
            context.fillRect(${top}, ${left}, ${parseInt(width)}, ${parseInt(
      height
    )});
            `;
    fs.writeFileSync("./code.js", code);
  }
}
复制代码

总结:DOM 如何生成的

  • 当服务端返回的类型是 text/html 时,浏览器会将收到的数据通过 HTMLParser 进行解析 (边下载边解析)
  • 在解析前会执行预解析操作,会预先加载 JS、CSS 等文件
  • 字节流 -> 分词器 -> Tokens -> 根据 token 生成节点 -> 插入到 DOM 树中
  • 遇到 js:在解析过程中遇到 script 标签,HTMLParser 会停止解析,(下载)执行对应的脚本。
  • 在 js 执行前,需要等待当前脚本之上的所有 CSS 加载解析完毕(js 是依赖 css 的加载)

image.png 一道小题,问下面的代码浏览器重排了几次(chrome新版浏览器为主)?

四、网络优化策略

  • 减少 HTTP 请求数,合并 JS、CSS,合理内嵌 CSS、JS
  • 合理设置服务端缓存,提高服务器处理速度。 (强制缓存、对比缓存)
// Expires/Cache-Control   Etag/if-none-match/last-modified/if-modified-since
复制代码
  • 避免重定向,重定向会降低响应速度 (301,302)
  • 避免使用 style 或 script 标签直接引入代码块,使用外部文件引入(这样做会增加请求数,手机端可以酌情引入样式、代码块)。
  • 使用 dns-prefetch,进行 DNS 预解析
  • 避免空的 href 和 src。原因是渲染过程中仍会引起加载动作。
  • 采用域名分片技术,将资源放到不同的域名下。接触同一个域名最多处理 6 个 TCP 链接问题。
  • 采用 CDN 加速加快访问速度。(指派最近、高度可用)
  • gzip 压缩优化 对传输资源进行体积压缩 (html,js,css)
// Content-Encoding: gzip
复制代码
  • 加载数据优先级 : preload(预先请求当前页面需要的资源) prefetch(将来页面中使用的资源) 将数据缓存到 HTTP 缓存中
<link rel="preload" href="style.css" as="style" />
复制代码
  1. 避免重定向,每次重定向会消耗大约 600 毫秒的时间。
  2. 使用静态资源粉鱼存放来增加并行下载数。原因还是浏览器同域资源的请求数有所限制。
  3. 使用 CDN 内容分发网络。CDN 可以提升静态资源的加载速度。
  4. 使用 CDN Combo,即将多个文件请求打包成一个文件,这样可以服用同一个 http 请求,加快资源下载速度。
  5. 使用 ajax 缓存:针对 get 请求,消息头添加 Expires,可以缓存响应。
  6. 使用 get 请求。原因是 post 请求先发送文件头,再发送 http 正文;get 请求只发送文件头。
  7. 减少 cookie 的大小并进行 cookie 隔离(即使用不同域名存放静态资源,这样就隔离了 cookie);设置合适的域级别和有效期。
  8. 减小 favicon.ico 的大小并缓存。
  9. 推荐使用异步 js 资源;异步的 js 资源不会阻塞文档解析。
  10. 合理拆分 css 及 js 资源,避免阻塞渲染。
  11. 避免在 import 方式加载 css 资源。原因是 import 方式需等解析到 @import 时才会加载指定的 css 资源,会大大延后 css 渲染完成的时间。
  12. 通过 Content-Encoding: gzip 响应头压缩资源。以下是基于 nginx 压缩文件。
etag on; # 开启etag验证
expires 7d; # 设置缓存过期时间为7天
gzip on; # 压缩资源
gzip_types text/plain application/javascriptapplication/x-javascripttext/css application/xm
复制代码

五、关键渲染路径

image.png 浏览器渲染页面流程

浏览器会先把 HTML 解析成 DOM树 计算 DOM 结构;然后加载 CSS 解析成 CSSOM;最后将 DOM 和 CSSOM 合并生成渲染树 layout Tree,浏览器根据页面计算 layout(重排阶段);最后浏览器按照 layout tree 绘制(painting,重绘阶段)页面。。

javascript-->Style-->Layout-->Paint-->Composite

  • 重排(回流)Reflow: 添加元素、删除元素、修改大小、移动元素位置、获取位置相关信息
重排是指 重新生成布局,重新排列元素 layout tree 某些 DOM 大小和位置发生了变化(页面的布局和几何信息发生了变化),浏览器重新渲染 DOM 的这个过程就是重排(DOM 回流),重排会消耗页面很大的性能,这也是虚拟 DOM 被引入的原因。
​
发生重排的情况:
1.第一次页面计算 layout 的阶段
2.添加或删除DOM节点,改变了 layout tree
3.元素的位置,元素的字体大小等也会导致 DOM 的回流
4.节点的几何属性改变,比如width, height, border, padding,margin等被改变
5.查找盒子属性的 offsetWidth、offsetHeight、client、scroll等,浏览器为了得到这些属性会重排操作。
6.框架中 v-if 操作也会导致回流的发生。
​
复制代码
  • 重绘 Repaint:页面中元素样式的改变并不影响它在文档流中的位置。
重绘是指 页面的样式发生了改变,但是 DOM 结构/布局没有发生改变。比如颜色发生了变化,浏览器就会对需要的颜色进行重新绘制。
​
发生重绘的情况
1.第一次页面 painting 绘制的阶段
2.元素颜色的 color 发生改变
3.动画开始和结束时其实都会有一次重绘,但是 transform 不会导致重排和重绘,因为,transform 位于复合层 Composite layer 层,而 width、left,hight 等元素属于 layout 层,没有重新经过 paint 的 绘画层。
​
复制代码
  • 添加、删除元素(回流+重绘)
  • 隐藏元素:display:none(回流+重绘);visibility:hidden(只重绘,不回流)
  • 移动元素:改变 top,left(不一定引起回流);将元素移动到另一个父元素中(回流+重绘)等
  • 改变 style:布局相关属性如 padding, border-width, font-size(回流+重绘);布局无关属性(只重绘,不回流)。可查看 csstriggers.com/ 以获取更多信息
  • 用户操作:改变浏览器大小,改变浏览器的字体大小等(回流+重绘)

区别:

重排会导致 DOM结构 发生改变,浏览器需要重新渲染布局生成页面,但是重绘不会引发 DOM 的改变只是样式上的改变,前者的会消耗很大的性能。

重排或者重绘的原理:

  • webkit 会把渲染树中的元素渲染成一个对象,每一个对象都代表了一个矩形的区域,包含宽度,高度,位置等集合信息。
  • 同时渲染的对象会受到 display 属性的影响,生成不同的渲染对象。

我们应当尽可能减少重绘和回流

  1. 强制同步布局问题

JavaScript 强制将计算样式和布局操作提前到当前的任务中

<div id="app"></div>
<script>
  function reflow() {
    let el = document.getElementById("app");
    let node = document.createElement("h1");
    node.innerHTML = "hello";
    el.appendChild(node);
    // 强制同步布局
    console.log(app.offsetHeight);
  }
  requestAnimationFrame(reflow);
</script>
复制代码
  1. 布局抖动(layout thrashing)问题

在一段 js 代码中,反复执行布局操作,就是布局抖动

function reflow() {
  let el = document.getElementById("app");
  let node = document.createElement("h1");
  node.innerHTML = "hello";
  el.appendChild(node);
  // 强制同步布局
  console.log(app.offsetHeight);
}
window.addEventListener("load", function() {
  for (let i = 0; i < 100; i++) {
    reflow();
  }
});
复制代码
  1. 减少回流和重绘
  • 脱离文档流
  • 渲染时给图片增加固定宽高
  • 尽量使用 css3 动画
  • 可以使用 will-change 提取到单独的图层中
  • 避免频繁操作 DOM,使用vue/react。
  • 避免使用 table 布局,因为 table 布局计算的时间比较长耗性能;
  • 样式集中改变,避免频繁使用 style,而是采用修改 class 的方式。
  • 样式的分离读写。设置样式style和读取样式的offset等分离开,也可以减少回流次数。
  • 将动画效果设计在文档流之上即 position 属性的 absolutefixed 上。使用 GPU 加速合成。
  • className 或 cssText 批量更新样式,避免单属性操作引起的频繁回流或重绘
  • 新创建的元素改完样式后,再插入文档;或者先将节点的 display 属性置为 none,调整样式后再置回显示状态
  • [慎用]使频繁回流、重绘的元素单独有一个 RenderLayer:借助 video 元素、WebGL、Canvas、CSS3 3D、CSS滤镜、z-index 大于某个相邻节点的元素都会有独立的 RenderLayer,比如通过添加 transform: translateZ(0); backface-visibility: hidden; 样式即可。

例题:

一道小题,问下面的代码浏览器重排了几次(chrome新版浏览器为主)?
box.style.width = "100px";
box.style.width = "100px";
box.style.position = "relative";
复制代码
​
你可能会觉得是3次,但是在当代浏览器中,浏览器会为上面的样式代码开辟一个渲染队列,将所有的渲染代码放入到队列里面,最后一次更新,所以重排的次数是1次。 问下面的代码会导致几次重排
​
box.style.width = "100px";
box.style.width = "100px";
box.offsetWidth;
box.style.position = "relative";
​
答案是2次,因为 offsetWidth 会导致渲染队列的刷新,才可以获取准确的 offsetWidth 值。最后 position 导致元素的位子发生改变也会触发一次回流。所以总共有2次。
​
复制代码

六.静态文件优化

  1. 图片优化

图片格式:

  • jpg:适合色彩丰富的照片、banner 图;不适合图形文字、图标(纹理边缘有锯齿),不支持透明度
  • png:适合纯色、透明、图标,支持半透明;不适合色彩丰富图片,因为无损存储会导致存储体积大
  • gif:适合动画,可以动的图标;不支持半透明,不适和存储彩色图片
  • webp:适合半透明图片,可以保证图片质量和较小的体积
  • svg 格式图片:相比于 jpg 和 jpg 它的体积更小,渲染成本过高,适合小且色彩单一的图标;

图片优化:

  • 避免空 src 的图片
  • 减小图片尺寸,节约用户流量
  • img 标签设置 alt 属性, 提升图片加载失败时的用户体验
  • 原生的 loading:lazy 图片懒加载
<img loading="lazy" src="./images/1.jpg" width="300" height="450" />
复制代码
  • 不同环境下,加载不同尺寸和像素的图片
<img
  src="./images/1.jpg"
  sizes="(max-width:500px) 100px,(max-width:600px) 200px"
  srcset="./images/1.jpg 100w, ./images/3.jpg 200w"
/>
复制代码
  • 对于较大的图片可以考虑采用渐进式图片
  • 采用 base64URL 减少图片请求
  • 采用雪碧图合并图标图片等
  1. HTML 优化
  • 语义化 HTML:代码简洁清晰,利于搜索引擎,便于团队开发
  • 提前声明字符编码,让浏览器快速确定如何渲染网页内容
  • 减少 HTML 嵌套关系、减少 DOM 节点数量
  • 删除多余空格、空行、注释、及无用的属性等
  • HTML 减少 iframes 使用 (iframe 会阻塞 onload 事件可以动态加载 iframe)
  • 避免使用 table 布局
  1. CSS 优化
  • 尽量不要使用 @import,由于@import 采用的是串行加载,会阻碍GUI渲染线程。
  • 减少伪类选择器、减少样式层数、减少使用通配符
  • 避免使用 CSS 表达式,CSS 表达式会频繁求值, 当滚动页面,或者移动鼠标时都会重新计算 (IE6,7)
background-color: expression( (new Date()).getHours()%2 ? "red" : "yellow" );
复制代码
  • 删除空行、注释、减少无意义的单位、css 进行压缩
  • 使用外链 css,可以对 CSS 进行缓存
  • 添加媒体字段,只加载有效的 css 文件
<link href="index.css" rel="stylesheet" media="screen and (min-width:1024px)" />
复制代码
  • CSS contain 属性,将元素进行隔离
  • CSS 代码量少可以使用内嵌式的style标签,减少请求。
  • 减少使用link,可以减少 HTTP 的请求数量。
  • CSS 选择器链尽可能短,因为CSS选择器的渲染时从右到左的。
  • 将写入的 link 请求放入到<head></head> 内部,一开始就可以请求资源,GUI同时渲染。
  1. JS 优化
  • 通过 async、defer 异步加载文件

img

  • 减少 DOM 操作,缓存访问过的元素
  • 操作不直接应用到 DOM 上,而应用到虚拟 DOM 上。最后一次性的应用到 DOM 上。
  • 使用 webworker 解决程序阻塞问题
  • IntersectionObserver
const observer = new IntersectionObserver(function(changes) {
  changes.forEach(function(element, index) {
    if (element.intersectionRatio > 0) {
      observer.unobserve(element.target);
      element.target.src = element.target.dataset.src;
    }
  });
});
function initObserver() {
  const listItems = document.querySelectorAll("img");
  listItems.forEach(function(item) {
    observer.observe(item);
  });
}
initObserver();
复制代码
  • 虚拟滚动 vertual-scroll-list
  • requestAnimationFrame、requestIdleCallback

img

  • 尽量避免使用 eval, 消耗时间久
  • 使用事件委托,减少事件绑定个数。
  • 尽量使用 canvas 动画、CSS 动画

5.字体优化

@font-face {
  font-family: "Bmy";
  src: url("./HelloQuincy.ttf");
  font-display: block;
  /* block 3s 内不显示, 如果没加载完毕用默认的   */
  /* swap 显示老字体 在替换 */
  /* fallback 缩短不显示时间, 如果没加载完毕用默认的 ,和block类似*/
  /* optional 替换可能用字体 可能不替换*/
}
body {
  font-family: "Bmy";
}
复制代码

FOUT(Flash Of Unstyled Text) 等待一段时间,如果没加载完成,先显示默认。加载后再进行切换。 FOIT(Flash Of Invisible Text)字体加载完毕后显示,加载超时降级系统字体 (白屏)

七. 优化策略

  • 关键资源个数越多,首次页面加载时间就会越长
  • 关键资源的大小,内容越小,下载时间越短
  • 优化白屏:内联 css 和内联 js 移除文件下载,较小文件体积
  • 预渲染,打包时进行预渲染
  • 使用 SSR 加速首屏加载(耗费服务端资源),有利于 SEO 优化。 首屏利用服务端渲染,后续交互采用客户端渲染

页面渲染类

  1. 把 css 资源放在 html 顶部,保证浏览器尽早完成页面渲染。
  2. 把 js 资源放在 html 尾部,避免加载和解析 js 过程中阻塞页面渲染。
  3. 避免在 html 中直接缩放图片。原因是缩放图片的动作会引起重排重绘。
  4. 减少 dom 节点的数量和深度,以提升 dom 树构建的速度。
  5. 避免使用 table, iframe 等慢元素。原因是 table 会等到它的 dom 树全部生成后再一次性插入页面中;iframe 内资源的下载过程会阻塞父页面静态资源的下载及 css, dom 树的解析。
  6. 避免运行耗时的 js;采用预加载方式加载资源,即当浏览器空闲候,再预加载资源(包含跳转页面的资源、新版本的资源等)。
  7. 避免使用运行较慢的 css 表达式或滤镜。

网络加载类

  1. 首屏数据提前请求,避免 js 文件加载后再请求数据。
  2. 首屏加载和按需加载,非首屏内容滚屏加载,保证首屏内容最小化。
  3. 模块化资源异步并行下载,可借助 webpack 达成 dynamic-import 实现。
  4. inline 首屏必要的 css 和 js,避免页面出现空白。
  5. 设置文件资源的 DNS 预解析,让浏览器提前解析获取静态资源的主机 IP,避免等到请求时才发起 DNS 解析请求。如
<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="//cdn.domain.com" />
复制代码

脚本类

  1. 尽量使用 id 选择器。原因选择 id 元素时执行速度最快。
  2. 尽量缓存 dom 对象,避免每次使用时需要从 dom 树中重新查找。
  3. 尽量使用事件代理,避免直接使用事件绑定。这样可以避免不必要的内存泄露及需要动态添加元素的事件绑定问题。
  4. 使用 touchstart 代替 click。因为移动端 touchstart 事件和 click 事件之间存在 300 毫秒的延时。
  5. 避免 touchmove, scroll 链接事件处理,事件触发频繁容易使页面卡顿,可每隔 16ms (60 帧的真间隔为 16.7ms)再触发事件。
  6. 避免使用 eval, with,使用 join 代替连接符 +,推荐使用 es6 的模板字符串。这样更安全。
  7. 尽量使用 es6+ 的特性来编程。这样更安全高效。

渲染类

  1. 使用 viewport 固定品目渲染,可以加载渲染过程,同时可以避免缩放导致的重排重绘。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
复制代码
  1. 避免各种形式的重排重绘。
  2. 使用 css3 动画,使用 transform: translateZ(0) 开启 GPU 加速,让动画更流畅。
  3. 合理使用 canvas 和 requestAnimationFrame 等更高效的动画实现方式,避免 setTimeout, setInterval 等方式直接处理连续动画。
  4. 使用 svg 代替图片,因为 svg 内容更小,结构更方便调整。
  5. 避免 float 比较耗时的布局方式,推荐使用固定布局或弹性布局。
  6. 避免过多的 font-size 声明,这样会增加字体的大小计算。

架构协议类

  1. 尝试使用 SPDY 和 http2 协议。SPDY 协议可复用连接,以加快传输过程,缩短资源加载时间。
  2. 使用后端数据渲染(数据回填到 html 中),这样可以避免空白页的出现,同时可以解决移动端 SEO 问题。
  3. 使用 Native View 代替 DOM,以便将页面内容渲染提升到接近客户端 Native 应用级别。

八.浏览器的存储

  • cookie: cookie 过期时间内一直有效,存储大小 4k 左右、同时限制字段个数,不适合大量的数据存储,每次请求会携带 cookie,主要可以利用做身份检查。
  • 设置 cookie 有效期
  • 根据不同子域划分 cookie 较少传输
  • 静态资源域名和 cookie 域名采用不同域名,避免静态资源访问时携带 cookie
  • localStorage: chrome 下最大存储 5M, 除非手动清除,否则一直存在。利用 localStorage 存储静态资源
function cacheFile(url) {
  let fileContent = localStorage.getItem(url);
  if (fileContent) {
    eval(fileContent);
  } else {
    let xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.onload = function() {
      let reponseText = xhr.responseText;
      eval(reponseText);
      localStorage.setItem(url, reponseText);
    };
    xhr.send();
  }
}
cacheFile("/index.js");
复制代码
  • sessionStorage: 会话级别存储,可用于页面间的传值
  • indexDB:浏览器的本地数据库 (基本无上限)
let request = window.indexedDB.open("myDatabase");
request.onsuccess = function(event) {
  let db = event.target.result;
  let ts = db.transaction(["student"], "readwrite");
  ts.objectStore("student").add({ name: "zf" });
  let r = ts.objectStore("student").get(5);
  r.onsuccess = function(e) {
    console.log(e.target.result);
  };
};
request.onupgradeneeded = function(event) {
  let db = event.target.result;
  if (!db.objectStoreNames.contains("student")) {
    let store = db.createObjectStore("student", { autoIncrement: true });
  }
};
复制代码

九、增加体验 PWA(Progressive Web App)

webapp 用户体验差(不能离线访问),用户粘性低(无法保存入口),pwa 就是为了解决这一系列问题,让 webapp 具有快速,可靠,安全等特点

  • Web App Manifest:将网站添加到桌面、更类似 native 的体验
  • Service Worker:离线缓存内容,配合 cache API
  • Push Api & Notification Api: 消息推送与提醒
  • App Shell & App Skeleton App 壳、骨架屏

十、性能分析

  1. performance:

Performance API 除了可以用于分析网页渲染过程的性能外,也可以用于分析某个方法的执行性能等。

  • performance.memory:内存占用的具体数据
  • performance.now():获取从 navigationStart 到当前时间的时间,可用于计算方法的执行时间
  • performance.mark(markName):给相应的视点做标记
  • performance.measure(name, startMark, endMark):计算方法的执行时间
  • performance.getEntriesByName(name):获取指定 measure
  • performance.clearMarks():清除标记
  • performance.clearMeasures():清除 measure
  1. profile:

console.profile() 或 console.profileEnd() 可用于分析 js 脚本的内存、cpu 占用情况。以下代码为使用 performance,profile 监控某方法执行性能的示例()。

const analyse = (fn, options) => {
  const { measureName } = options;
​
  performance.mark(`${measureName}-start`);
  console.profile();
​
  fn();
​
  console.profileEnd();
​
  // 标记一个结束点
  performance.mark(`${measureName}-end`);
​
  // 标记开始点和结束点之间的时间戳
  performance.measure(
    measureName,
    `${measureName}-start`,
    `${measureName}-end`,
  );
​
  // 获取所有名称为mySetTimeout的measures
  const measures = performance.getEntriesByName(measureName);
  const measure = measures[0];
  console.log(`${measureName} milliseconds: ${measure.duration}`);
​
  // 清除标记
  performance.clearMarks();
  performance.clearMeasures();
};
​
analyse(() => {
  for (let i = 0; i < 1000; i++){
    console.log(i + 1);
  };
}, {
  measureName: 'cycle'
});
复制代码

Guess you like

Origin juejin.im/post/7047078446760984612