Web前端性能优化自查清单

前言

一份简洁、纯粹的Web前端性能优化清单。每个优化点都包含有概念、实操和参考资料。面试、实战两相宜。

这是一个大工程。在正式开始之前,先统一下语言,澄清每一部分的目的和要求,防止跑偏。

  • 概念:把官话翻译成能看懂、能记住的人话,原则上易读性 > 专业性

  • 实操:自己操作一遍,不做云玩家;记录核心实现,方便CV

  • 参考资料:信息来源选用一手资料,以便保证信息的完整性、准确性和时效性。除非看一手的理解不了……

一、网络层面

1. DNS预解析

概念

DNS-prefetch 是一种 DNS 预解析技术。它会在请求跨域资源之前,预先解析并进行DNS缓存,以减少真正请求时DNS解析导致的请求延迟。对于打开包含有许多第三方连接的网站,效果明显。

实操

添加ref属性为“dns-prefetch”的link标签。一般放在在html的head中。

1
<link rel="dns-prefetch" href="//xxx.download.com">

href的值就是要预解析的域名,对应后面要加载的资源或用户有可能打开链接的域名。

备注

同理,也有“ TCP/IP预连接”,叫preconnect。参考资料中有完整的描述。

参考资料

2. 应用浏览器缓存

概念

浏览器缓存是浏览器存放在本地磁盘或者内存中的请求结果的备份。当有相同请求进来时,直接响应本地备份,而无需每次都从原始服务器获取。这样不仅提升了客户端的响应效率,同时还能缓解服务器的访问压力。

其间,约定何时、如何使用缓存的规则,被称为缓存策略。分为强缓存和协商缓存。

整个缓存执行的过程大致如下:

①. 请求发起,浏览器判断本地缓存,如果有且未到期,则命中强缓存。浏览器响应本地备份,状态码为200。控制台Network中size那一项显示disk cache;

②. 如果没有缓存或者缓存已过期,则请求原始服务器询问文件是否有变化。服务器根据请求头中的相关字段,判断目标文件新鲜度;

③. 如果目标文件没变更,则命中协商缓存,服务器设置新的过期时间,浏览器响应本地备份,状态码为304;

④. 如果目标文件有变化,则服务器响应新文件,状态码为200。浏览器更新本地备份。

上述过程有几个关键点

  • 如何判断缓存是否过期?

    浏览器读取缓存的请求结果中响应头的Expires 和Cache-Control,与当前时间进行比较。

    其中,Expires是HTTP 1.0的字段,值是一个是绝对时间。

    1
    Expires: Tue, 18 Jan 2022 09:53:23 GMT

    比较绝对时间,有一个弊端,它依赖计算机时钟被正确设置。

    为了解决这个问题,HTTP1.1 新增了Cache-Control字段,它的值是一个是相对时间。

    1
    Cache-Control: max-age=60  //单位是秒
  • 如何判断文件是否变化?

    首先可以通过比较 最后修改时间。

    1
    2
    3
    4
    // 缓存结果的 响应头
    Last-Modified: Mon, 10 Jan 2022 09:06:14 GMT
    // 新请求的 请求头
    If-Modified-Since: Mon, 10 Jan 2022 09:06:14 GMT

    浏览器取出缓存结果中Last-Modified的值,通过If-Modified-Since上送到服务端。与服务器中目标文件的最后修改时间做比较。

    再者可以通过比较 Etag。

    Etag实体标签是附加到文档上的任意标签(引用字符串)。它们可能包含了文档的序列号或版本名,或者是文档内容的校验和及其他指纹信息。当发布者对文档进行修改时,会修改文档的实体标签来说明这是个新的版本。

    从响应头的ETag取值,通过请求头的If-None-Match上送,与服务器目标文件的Etag标签比对。

    1
    2
    3
    4
    // 缓存的 响应头
    ETag: "61dbf706-142"
    // 上送的 请求头
    If-None-Match: "61dbf706-142"

    和上面一样,新增的字段也是为了解决前一种方案的某些缺陷:

    • 有些文档可能会被周期性地重写(比如,从一个后台进程中写入),但实际包含的数据常常是一样的。尽管内容没有变化,但修改日期会发生变化。
    • 有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如对拼写或注释的修改)。
    • 有些服务器无法准确地判定其页面的最后修改日期。
    • 有些服务器提供的文档会在亚秒间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了。
  • 如果两个版本的字段同时存在,怎么办?

    出于浏览器兼容方面的考虑 ,一般两组字段会被同时使用。他们没有优先级一说,取并集。

    同时出现时,只有当两个条件都满足,才会命中相应缓存。

实操

缓存是web服务器和浏览器的核心能力,主流的web服务框架 nginx、koa-static等都内置有上述缓存策略的实现。开箱即用,无需额外编程或配置。

以Nginx举例。强缓存的配置字段是expires,它接受一个数字,单位是秒。

1
2
3
4
5
6
7
8
9
10
server {
listen 8080;
location / {
root /Users/zhp/demo/cache-koa/static;
index index.html;
# 注意try_files会导致缓存配置不生效
# try_files $uri $uri/ /index.html;
expires 60;
}
}

实际工作中确实配置一下就好了,但这体现不出什么知识点。为了加深印象,我这用koa简陋的模拟了一下,算是对上面那些知识点的验证。

下面是一个极简的静态资源服务,不带缓存的。

1
2
3
4
5
6
app.use(async (ctx) => {
// 1.根据访问路径读取指定文件
const content = fs.readFileSync(`./static${ctx.path}`, "utf-8");
// 2.设置响应
ctx.body = content;
});

这种情况,无论访问多少次都是不进缓存的。

现在,在响应头加上强缓存所需的ExpriseCache-Control字段

1
2
3
4
5
6
7
8
9
app.use(async (ctx) => {
// 1.根据访问路径读取指定文件
const content = fs.readFileSync(`./static${ctx.path}`, "utf-8");
// 2.设置缓存
ctx.response.set("Cache-Control", "max-age=60");
ctx.response.set('Exprise', new Date(new Date().getTime()+60*1000));
// 3.设置响应
ctx.body = content;
});

查看Network,响应头会多出下面两个字段,且间隔60秒内的请求会走缓存,符合预期。

1
2
Expires: Tue, 18 Jan 2022 10:05:09 GMT
Cache-Control: max-age=60

备注

抱着引用一手权威资料的想法,扒了《HTTP权威指南》,但读感着实差强人意。新手建议《图解HTTP》起手,要友好很多。

参考资料

3. 静态资源CDN

概念

CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

核心功效总结起来就两点:

①. 通过负载均衡技术 ,为用户的请求选择最佳的服务节点;

②. 通过内容缓存服务,提高用户访问响应速度。

实操

普通玩家:选择一个CDN服务商,看它提供的使用文档。通过配置域名和源站,代理到自己的静态资源服务器。

高级玩家:自建CDN服务器,balabal……

参考资料

4. 开启Gzip

概念

gzip是GNUzip的缩写,最早用于UNIX系统的文件压缩。HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术,web服务器和客户端(浏览器)必须共同支持gzip。gzip压缩比率在3到10倍左右,可以大大节省服务器的网络带宽。

实操

实际操作过程中分为动态压缩和静态压缩。

  • 动态压缩。指当收到请求后,服务器实时压缩然后输出数据流。服务器存放的是css/js文件。
    Nginx的httpGzip模块,支持该功能。主要配置如下:

    1
    2
    3
    4
    5
    6
    # 开启或者关闭gzip模块
    gzip on;
    # 设置允许压缩的页面最小字节数。建议设置成大于1k的字节数,小于1k可能会越压越大。
    gzip_min_length 1024;
    # 匹配MIME类型进行压缩,(无论是否指定)"text/html"类型总是会被压缩的。
    gzip_types text/plain application/x-javascript text/css text/html application/xml;
  • 静态压缩。服务器一开始存放的就是已经压缩好的文件,当接受请求后直接响应压缩资源,而不是收到请求后才压缩。

    使用Webpack + Nginx的实现:

    ①. 安装并应用compression-webpack-plugin压缩插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // ## 安装 ##
    // 注意高版本会报错 Cannot read property 'tapPromise' of undefined
    npm i --save-dev compression-webpack-plugin@5.0.1

    // ## webpack配置 ##
    // vue.config.js
    const CompressionPlugin = require("compression-webpack-plugin");

    module.exports = {
    configureWebpack:{
    plugins: [
    new CompressionPlugin()
    ]
    }
    }

    ②. 执行构建npm run build

    打包完成后,在dist目录下会多出.gz结尾的压缩文件

    ③. Nginx配置开启gzip_static

    1
    2
    3
    4
    5
    6
    7
    8
    9
    http{
    gzip_static on;
    server {
    listen 8082;
    location / {
    root /Users/zhp/demo/demo-externals/dist;
    }
    }
    }
  • 结果验证

    在Response Header中看到有Content-Encoding: gzip,说明服务器配置生效;

    在Network的Size列看数据比服务器上源文件要小,说明浏览器支持,Gzip生效。

备注

gzip_static的优先级高于gzip。当gzip和gzip_static都开启时,nginx会优先匹配.gz文件,然后才走动态压缩。

参考资料:

5. 使用高版本的HTTP协议

概念

从1.0到1.1再到如今的2.0,HTTP协议在持续迭代中,变的更快更强。

其间的变更内容多且硬核,这里出于解释高版本优势的目的,简单的列举一二,HTTP/1.1的持久连接和管道化技术、2.0的多路复用和首部压缩。

  • 持久连接

    HTTP 协议的初始版本中,每进行一次 HTTP 通信就要断开一次 TCP连接。为了减少了TCP 连接的重复建立和断开所造成的额外开销。 HTTP/1.1 和一部分的 HTTP/1.0 想出了 持久连接(HTTP Persistent Connections,也称为 HTTP keep-alive 或HTTP connection reuse)的方法。持久连接的特点是,只要任意一端 没有明确提出断开连接,则保持 TCP 连接状态。

  • 管道化

    从前,发送请求后需等待并收到响应,才能发送下一个请求。管道化技术允许客户端同时并行发送多个请求,而不需要一个接一个地等待响应。

    image-20220207195536838

  • 多路复用

    HTTP/1.1,即便是通过管道同时发送了多个请求,服务端也是按请求的顺序依次给出响应的。客户端在未收到之前所发出所有请求的响应之前,将会阻塞后面的请求(排队等待),这称为”队头堵塞”(Head-of-line blocking)。

    HTTP/2引入二进制数据帧的概念,对数据进行顺序标识,浏览器收到数据之后,可以按照序列对数据进行合并,使得服务端可以并行的传输数据。

    解决了顺序问题,我们在一个TCP连接上,就可以向对方不断发送一个个的消息,这里每一个消息看成是一帧,而每一帧有个stream identifier的字段标明这一帧属于哪个“流”,然后在对方接收时,根据stream identifier拼接每个“流”的所有帧组成一整块数据。

    我们把HTTP/1.x每个请求都当作一个“流”,那么请求化成多个流,请求响应数据切成多个帧,不同流中的帧交错地发送给对方,这就是HTTP/2中的多路复用。

    最终达成的效果就是,同一域名不管访问多少文件多少请求,也只需建立一路连接

  • 首部压缩

    在HTTP/1.x中首部是没有压缩的,gzip只会压缩body,HTTP/2提供了首部压缩方案。一般轮询请求首部,特别是cookie占用很多大部份空间,首部压缩使得整个HTTP数据包小了很多,传输也就会更快。

实操

主流Web服务器 Nginx、Tomcat等都有对HTTP/2的支持,具体配置自行参考官方文档(笔者驾驭不住😂)。

备注

mark一下如何查看协议版本信息。在Network中对着表头右键,弹窗中勾选Protocol这一项。

WX20220112-200043@2x

参考资料

二、代码层面

1. 优化DOM操作

概念

众所周知,浏览器的渲染成本是极其昂贵的。通过合并DOM操作,可以避免频繁的触发重排重绘,以提升渲染效率。

优化DOM操作的最佳实践,莫过于大名鼎鼎的虚拟DOM。

virtual DOM 虚拟DOM,用普通JS对象来描述DOM结构,因为不是真实DOM,所以称之为虚拟DOM

它的价值在于:

①. 查找 JS 对象的属性要比查询 DOM 树的开销要小;

②. 当数据驱动频繁触发DOM操作的时候,所有变化先反映在这个 JS 对象上。最终在一个宏任务(EventLoop机制)中统一执行所有变更,达成合并DOM操作的效果;

③. 可以方便的通过比较新旧两个虚拟DOM(Diff算法),最大程度的缩小DOM变更范围。

实操

Vue和React都引入有虚拟DOM的概念,加上数据驱动,使得我们在使用框架后,已经不需要再关注具体的DOM操作了。

备注

这一节本应是一堆原生DOM操作的例子,但在Vue、React一统天下的当下讲太多这些,着实会显得有些过时。

参考资料

2. 事件委托

概念

简单来讲,就是当我们绑定事件时,不直接绑到目标元素,而是绑到其父/祖先元素上的绑事件策略。

这样做有两个好处:①. 页面监听的事件少;②. 当新增子节点时,不需要再绑定事件。

实操

以”鼠标放到li上对应的li背景变灰“这个需求场景举例

  • 正常绑事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
<li>item5</li>
<li>item6</li>
</ul>
<script>
$("li").on("mouseover", function () {
$(this)
.css("background-color", "#ddd")
.siblings()
.css("background-color", "white");
});
</script>
  • 利用事件委托:
1
2
3
4
5
6
$("ul").on("mouseover", function (e) {
$(e.target)
.css("background-color", "#ddd")
.siblings()
.css("background-color", "white");
});

3. 防抖与节流

概念

防抖与节流都是为了优化单位时间内大量事件触发,存在的性能问题。它们只是效果不同,适用场景不同。

  • 防抖。单位时间多次连续触发,最终只执行最后的那一次。核心原理是延迟执行,期间但凡有新的触发就重置定时器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function debounce(fn) {
    // 1、创建一个标记用来存放定时器的返回值
    let timeout = null;
    return function() {
    // 2、每次当用户点击/输入的时候,把前一个定时器清除
    clearTimeout(timeout);
    // 3、然后创建一个新的 setTimeout,
    // 这样就能保证点击按钮后的 interval 间隔内,如果用户还点击了的话,就不会执行 fn 函数
    timeout = setTimeout(() => {
    fn.call(this, arguments);
    }, 1000);
    };
    }

    经典应用场景:搜索框中的实时搜索,等待用户不再输入内容后再做接口查询。

  • 节流。单位时间内事件仅触发一次。核心原理是加锁,只有满足一定间隔时间才执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function throttle(fn) {
    // 1、通过闭包保存一个标记
    let canRun = true;
    return function(...args) {
    // 2、在函数开头判断标志是否为 true,不为 true 则中断函数
    if(!canRun) {
    return;
    }
    // 3、将 canRun 设置为 false,防止执行之前再被执行
    canRun = false;
    // 4、定时器
    setTimeout( () => {
    fn.call(this, args); //如果需要立即执行,把改行移到定时器外层
    // 5、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
    canRun = true;
    }, 1000);
    };
    }

    经典应用场景:滚动事件等高频触发的场景;按钮防重复点击等

实操

实际应用时,我们可以直接使用上面的函数,也可以引用第三方类库,比如lodash。

  1. 安装

    1
    npm i lodash.debounce
  2. 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // xxx.vue
    <template>
    <div>
    <input type="text" @input="onInput">
    </div>
    </template>

    <scrit>
    import debounce from "lodash.debounce"
    export default{
    methods:{
    onInput:debounce((event)=>{
    console.log(event)
    },1000)
    }
    }
    </script

4. 图片懒加载

概念

图片懒加载是针对图片加载时机的一种优化,在一些图片量比较大的网站(比如电商网站首页,或者团购网站、小游戏首页等),如果我们尝试在用户打开页面的时候,就把所有的图片资源加载完毕,那么很可能会造成白屏、卡顿等现象。

懒加载的意思就是让浏览器只加载可视区内的图片,可视区外的大量图片不进行加载,当页面滚动到后面去的时候再进行加载。避免资源浪费的同时,可以使页面加载更流畅。

实操

原理很简单,就是先将img src置空,等到图片进入视图区域时再设置src,加载相应资源。但我们没必要真的去实现这些,那是重复造轮子。该类成熟的插件/组件有很多,比如Vant的懒加载

1
2
3
4
5
6
// main.js
import { Lazyload } from 'vant';
app.use(Lazyload);

// xxx.vue
<img v-for="img in imageList" v-lazy="img" />

备注

图片只是载体,懒加载贯彻的是按需加载的思路。举一反三,分页查询、路由懒加载、模块异步加载,都是该类别的常用优化。

参考资料

三、 构建层面

构建工具有很多,这里单练Webpack。

弱水三千只取一瓢

1. 路由懒加载

概念:

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

实操

下面是VueRouter关于路由懒加载的官方示例

1
2
3
4
5
6
7
8
9
10
// 将
// import UserDetails from './views/UserDetails'
// 替换成
const UserDetails = () => import('./views/UserDetails')

const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})

核心实现就两点:

①. 使用了ES6 的动态导入方法import(),异步的加载模块;

②. 打包工具,在构建时自动识别并打包成单独的代码块。

我们还可以通过行内注释/* webpackChunkName: "about" */(Webpack语法),指定代码块的名称,和把多个路由源码构建到同一个块中。

1
2
3
4
5
6
7
8
9
// router.js
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}

参考资料

2. Externals排除依赖

概念

Webpack的externals配置项允许我们从输出的 bundle 中排除指定依赖,排除的依赖不参与构建。

通常用于配合较大体积第三方依赖使用CDN的场景。

实操

以在vue-cli项目中 CDN vue举例

  1. 首先在public/index.html添加script引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // public/index.html
    <!DOCTYPE html>
    <html lang="">
    <head>
    ...
    <script src="https://lib.baomitu.com/vue/2.6.11/vue.min.js"></script>
    </head>
    <body>
    ...
    </body>
    </html>
  2. 使用webpack配置项externals排除vue的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // vue.config.js
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

    module.exports = {
    configureWebpack:{
    plugins: [
    new BundleAnalyzerPlugin() // 用于输出下图中的打包分析报告 npm run build --report
    ],
    externals: {
    vue: 'Vue',
    },
    }
    }
  3. 使用BundleAnalyzerPlugin(打包分析插件)验证结果

    下面两张图,就是改动前后,两次执行npm run build --report得到的分析结果。

    image-20220125203908595

    image-20220125204600775

    通过比较可以看到,包体积整体减少了200多Kb,且chunk-vendors.js中移除了vue相关依赖。符合预期。

参考资料

3. TreeShaking按需引入

概念

TreeShaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

概念早就有了,实现的话则是在ES6之后。主要得益于ES6 Module模块的编译时加载,使得静态分析成为可能。

实操

Webpack 4 正式版本,扩展了该项能力。在vue-cli创建的项目中我们不需要任何额外配置,就有效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// assets/util
const funcA =()=>{
console.log("this is funcA")
}
const funcB =()=>{
console.log("this is funcB")
}
export {
funcA,
funcB
}

// app.vue
import { funcA } from "./assets/util";
export default {
created() {
funcA();
},
};

执行npm run build,点开dist/app.xxx.js。可以看到只有funcA 没有B 。符合预期。

但,当improt第三方插件时,实际并没有生效。比如lodash

1
2
import debounce from 'lodash/debounce'; // 3.35kb
import { debounce } from 'lodash'; // 72.48kb

因为,它的生效需要满足一些条件:

  • 使用 ES2015 模块语法(即 importexport)。
  • 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档)。
  • 在项目的 package.json 文件中,添加 "sideEffects" 属性。
  • 使用 mode"production" 的配置项以启用更多优化项,包括压缩代码与 tree shaking。

需要注意的是,不是说不支持TreeShaking,就不能按需引入了。在TreeShaking之前,实现该类效果的插件比比皆是,比如babel-plugin-import。

1
2
3
4
5
6
7
import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);

↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
ReactDOM.render(<_button>xxxx</_button>);

参考资料

四、高手进阶

此处列举一些小众偏门、门槛高、格局大的优化手段。

1. SSR

概念

SSR是Server Side Render(服务端渲染)的简称,与之相对应的是Client Side Render(客户端渲染)。

  • 服务端渲染:在服务端完成页面插值/数据组装,直接返回包含有数据的页面。
  • 客户端渲染:客户端分别请求页面静态资源和接口数据,然后操作DOM赋值到页面。

其实,Web世界诞生的初始,只有服务端渲染这一种方式。 那时.net、jsp如日中天,那时还只有一种程序员,不分前后端。直到Ajax技术的出现,允许人们不刷新页面的获取数据,客户端渲染的大门就此打开,一发而不可收拾。前后端分离、单页应用的流行,更是一步步的把客户端渲染的疆域推向极致。

现如今,SSR一般只存在于对首屏时间有苛刻要求、以静态内容为主和需要SEO的场景。

实操

传统的服务端渲染使用后端模板系统或字符串模板引擎就能完成,这里选用复杂度和难度更高的SPA SSR举例。

下面是Vue SSR官方示例,只是把express换成了我更熟悉的koa。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ssr.js
import Koa from "koa";
import { createSSRApp } from "vue";
import { renderToString } from "vue/server-renderer";

const app = new Koa();

app.use(async (ctx) => {
const vueApp = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
});
const html = await renderToString(vueApp);
const result = `
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`;
ctx.body = result;
});

app.listen(3000, () => {
console.log("starting at port 3000");
});

逻辑异常简单,①. 创建单页应用;②. vue实例转字符串;③. 拼接html并响应。

node ssr.js,然后访问localhost:3000,我们就能如期看到页面上那个值为1的按钮了。

但这还没完事,此时点击按钮,数字是不会变化的。Vue还需要一个hydration(水合,也有人叫客户端激活)的步骤。

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。

说白了就是客户端仍然需要实例化vue app,以加载运行DOM事件等非静态代码。

hydration第一步:新增client.js。内容和上面第一步”创建单页应用“一样。

1
2
3
4
5
6
7
8
9
// client.js
import { createSSRApp } from "vue";

const vueApp = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
});

vueApp.mount('#app');

hydration第二步:挂载到html。在html head中引入client.js。

1
<script type="module" src="/client.js"></script>

由于引入有js文件,咱们还需要启动一个静态资源服务,保证页面能加载到client.js。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Koa from "koa";
import koaStatic from "koa-static";
import { createSSRApp } from "vue";
import { renderToString } from "vue/server-renderer";

const app = new Koa();

// 静态资源中间件,确保能加载到client.js
app.use(koaStatic("."));

app.use(async (ctx) => {
const vueApp = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
});
const html = await renderToString(vueApp);
const result = `
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module" src="/client.js"></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`;
ctx.body = result;
});

app.listen(3000, () => {
console.log("starting at port 3000");
});

重启服务,刷新页面,数字就能动起来了。

例子终归是例子,只是冰山一角,只是基础示意。在实际应用中还少不了,预取数据、解决状态污染、协同构建等一系列问题。总之,从头搭建一个服务端渲染的应用是相当复杂的。好在社区有成熟的SSR 解决方案,比如Vue官方推荐的Nuxt.js。

备注

记录一个报错。如果遇到node执行报错SyntaxError: Cannot use import statement outside a module,那么请升级node版本并在package.json中设置”type”: “module”。

需要通过 .mjs 文件扩展名、package.json "type" 字段、或 --input-type 标志告诉 Node.js 将 JavaScript 代码视为 ECMAScript 模块。

参考资料

2. Web Workers

概念

web worker 是运行在后台的 JavaScript,不会影响页面的性能。

原理就是开子线程

Worker接口会生成真正的操作系统级别的线程,线程可以执行任务而不阻塞 UI 线程。

一般用于处理像密集型运算等耗费 CPU 资源的任务。

实操

无米之炊。我这阅历并没有遇到需要Worker的场景,仅说下自己联想到的唯二信息:①.有些插件比如psfjs有这块的应用,因为它的构建结果中有xxx.worker.js;②. Node有线程相关的API(child_process),在构建的场景有较多应用。

备注

列出此项与我更多的意义可能在于,时刻提醒自己:打开格局,不要思维定式。

JS是单线程的,但浏览器不是。我们是Jser,但更是Coder,不应该也不能,把自身的视野和认知仅限定在单线程或某个特定的疆域内。

参考资料

3. 建设性能优化体系

概念

自查清单只是提醒我们在开发过程中应该注意或者需要关注的一些点。如果遇到专项优化任务,那么肯定会有数值化、可验证对比等方面的要求。这时候就需要我们暂时抛开细碎的优化手段,站在更高的层级,去构建一个有目标、可验证、成体系的性能优化体系。

一般包含:指标选定、采集上报、现状分析、优化方案、测试方案、性能评估及预警等环节。

实操

有生之年

五、写在最后

本文断断续续写了1个多月,一度难产。消耗着我写作激情的同时,更是差点断送了我定期总结的大好习惯。

分析了很多原因,从思维模式到遣词造句,从人性驱动到性格剖析。在此处做个反思总结,引以为戒。

  1. 目标错误,做了太多无用功。一度致力于解释为什么1+1=2,纠结于如何浅显易懂的讲解那些大家都懂得的道理;
  2. 钻了细枝末节的牛角尖。时常纠结一词一句的选用,比如该用 “应用“ 还是 ”使用“、该用逗号还是句号;
  3. 缺少遣词造句的积累。看东西时,一目十行只取其意,轮到自己表达了,绞尽脑汁生搬硬造;
  4. 逻辑思维的混乱无序。总是跳跃性的想到很多的点,然后再去归纳分类。而不是”金字塔“式的分层思考、有序表达。

[苦笑]着实没什么写东西的天赋,但谁让自己喜欢呢。很认可阮一峰老师的一个观点:

很多人建议,寻找人生方向时,你应该听从自己的内心,寻找真正热爱的事情。我现在觉得,更现实的建议应该是,寻找你愿意忍受的痛苦。 你在哪一个方向上,愿意心甘情愿地、经年累月地吃苦,具有最大的忍耐,”虽九死其尤未悔”,那就是你应该选择的方向。

嗯,很庆幸编程和写作能成为我的方向。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!