不知不觉, 已经来到了最后的下篇 其实我写的东西你如果认真去看, 跟着去写, 应该能有不少的收获.
最近一些跨平台技术, React-native 和 flutter 之类的, 比较火. 但是, 我还是不准备把它们放进来, 因为那是为做 App 而生, 我想把 Electron 这个桌面端跨平台的技术放进来. 理由是什么, 后面说
这是上篇和中篇, 如果你是第一次看这个系列文章, 欢迎去从头开始学习:
前端 20 个灵魂拷问 彻底搞明白你就是中级前端工程师 [上篇]
前端 20 个灵魂拷问 彻底搞明白你就是中级前端工程师 [中篇]
以及一些比较不错的文章:
从零编写一个 React 框架
我们为什么要熟悉这些通信协议
单页面应用 SPA 原理
9102 年: 手写一个 React 脚手架 [优化极致版]
性能优化不完全手册
Electron 跨平台入门系列
上面的文章, GitHub 上, 都有对应的源码.
进入正题
一千个人眼里有一千个哈姆雷特, 我们做不到完美
每个人评判的标准不一样, 我们唯有拿出碾压这个层级的能力的时候, 才能堵住质疑者的嘴. 当然, 我们不做技术杠精, 技术本身没有好坏. 不喜欢就不理会
最后问题, 我准备如下内容:
前端的性能优化方向
从传输层面去优化的方向
预解析地址 首次请求解析地址如果没有缓存 那么可能消耗 60-120ms
性能优化不完全手册这里面有介绍
preload 预请求必要内容, prefetch 预请求可能需要内容
这种请求方式不会阻塞浏览器的解析, 而且能将预请求的资源缓存起来, 而且可以设置 crossorgin 进行跨域资源的缓存, 不会推迟首屏的渲染时间, 还会加快后面的加载时间, 因为后面的本身需要的资源会直接从缓存中读取, 而不会走网络请求.
使用 preload 前, 在遇到资源依赖时进行加载:
使用 preload 后, 不管资源是否使用都将提前加载:
可以看到, preload 的资源加载顺序将被提前:
使用 preload 后, Chrome 会有一个警告:
preload 和 prefetch 混用的话, 并不会复用资源, 而是会重复加载.
若不确定资源是必定会加载的, 则不要错误使用 preload, 以免本末倒置, 给页面带来更沉重的负担.
preload 加载页面必需的资源如 CDN 上的字体文件, 与 prefetch 预测加载下一屏数据, 兴许是个不错的组合.
preload 和 prefetch 详解 这篇文章写得很棒 感谢作者
减少传输次数
部分图片 base64 处理, 然后使用雪碧图. 多张图拼成一张传输
当然 base64 这个东西慎用, 实际开发中它表现并那么好
减少传输体积
例如后端返回数据:"该用户没有拥有权限"
可以改成: 0
约定优于配置的思想一定要有
使用 probbuffer 协议
ProtoBuffer 是由谷歌研发的对象序列化和反序列化的开源工具
它的本质就是将一段数据序列化, 转变成二进制形式传输
然后另外的服务器端或者客户端接受到之后 反序列化, 转换成对应的数据格式(JSON)
好像还有人没有听说这个传输协议 其实它传输过程就是 2 进制流的形式
用得最多的是和 GRPC 配合 Go 语言或者服务器之间传输数据
例如 IM 应用, 每个 IM 应用都是一个服务端 也是一个客户端
那么对于这种频繁传输数据的时候, 可以使用 protobuffer 传输协议
protobuffer 下载
protobuffer 有几个优点:
1. 平台无关, 语言无关, 可扩展;
2. 提供了友好的动态库, 使用简单;
3. 解析速度快, 比对应的 xml 快约 20-100 倍;
4. 序列化数据非常简洁, 紧凑, 与 xml 相比, 其序列化之后的数据量约为 1/3 到 1/10.
protobuffer.JS https://github.com/protobufjs/protobuf.js - 我们可以使用这个库来解析
protobuf.JS 提供了几种方式来处理 proto.
直接解析, 如 protobuf.load("awesome.proto", function(err, root) {...})
转化为 JSON 或 JS 后使用, 如 protobuf.load("awesome.json", function(err, root) {...})
当然我们一般转换成. JS 后使用
vue 使用 protobuffer 我这里不做大篇介绍, 因为有人完全用不到
代码层次优化:
封装数据对象
可以用对象进行大数据封装, 尽量用对象 key-value 形式封装
如果需要对象遍历 其实也有很多种方法可以做到
用对象有个好处 就是数据量大起来但是需要查找的时候会非常快
避免书写耗时的同步代码
不管前端怎么发展, JS 主线程是单线程, 并且与 GUI 渲染线程互斥还是没有变
为什么?
因为 JS 可以进行 dom 操作 为了防止在渲染过程出现 dom 操作而造成不可预见后果
现代框架的底层其实还是 dom 操作 并且直接的 dom 操作比数据驱动要快多!
例如:
- for(let i=0; i< 100000; i++){
- console.log(i)
- }
- console.log(1)
for 循环其实很快, 但是走完这 100000 次循环的耗时, 到打印出 1, 有可能超过 100ms
那么如果这个打印输出 1 是一个用户交互操作 就会让用户有了延迟卡顿的现象
所谓的卡, 并不是电脑或者手机带不动我们的代码, 而是 JS 线程和 GUI 渲染互斥造成的假象.(大部分是这情况, 也有配置特别低的)
如果非要同步代码的场景?
那么我建议 ES6 的异步方案, 或者改变实现方案, 因为大部分性能方案优化是卡在这个点.
手机端白屏, 持久化存储等解决网络传输慢等方案
淘宝等 task-slice 方案
淘宝 task-slice 方案 https://juejin.im/post/5d37ce6f6fb9a07efd474d78
先不说这篇文章实现最终效果怎样, 但是这种思想在前端里是可以大量使用的, Go 语言里就有切片
渲染任务分割后打开性能调试面板
可以看到是一点点渲染出来的 也算是加快了首屏渲染吧!
切片队列的核心代码:
- function* sliceQueue({ sliceList, callback }) {
- let listOrNum = (isNum(sliceList) && sliceList) || (isArray(sliceList) && sliceList.length);
- for (let i = 0; i < listOrNum; ++i) {
- const start = performance.now();
- callback(i);
- while (performance.now() - start < 16.7) {
- yield;
- }
- }
- }
跟我的 React 框架编写的每帧清空渲染队列有点类似:
- /**
- * 队列 先进先出 后进后出 ~
- * @param {Array:Object} setStateQueue 抽象队列 每个元素都是一个 key-value 对象 key: 对应的 stateChange value: 对应的组件
- * @param {Array:Component} renderQueue 抽象需要更新的组件队列 每个元素都是 Component
- */
- const setStateQueue = [];
- const renderQueue = [];
- function defer(fn) {
- //requestIdleCallback 的兼容性不好, 对于用户交互频繁多次合并更新来说, requestAnimation 更有及时性高优先级, requestIdleCallback 则适合处理可以延迟渲染的任务~
- // if (Windows.requestIdleCallback) {
- // console.log('requestIdleCallback');
- // return requestIdleCallback(fn);
- // }
- // 高优先级任务
- return requestAnimationFrame(fn);
- }
- export function enqueueSetState(stateChange, component) {
- if (setStateQueue.length === 0) {
- // 清空队列的办法是异步执行, 下面都是同步执行的一些计算
- defer(flush);
- }
- ...//dosomething
- }
从零编写一个 react 框架 https://github.com/JinJieTan/mini-react
数据持久化存储
PWA, 渐进式 web 应用
将数据资源储存在缓存中, 每次请求前判断是否在 Service Worker 中, 如果没有再请求网络资源
PWA 的主要特点包括下面三点:
可靠 - 即使在不稳定的网络环境下, 也能瞬间加载并展现
体验 - 快速响应, 并且有平滑的动画响应用户的操作
粘性 - 像设备上的原生应用, 具有沉浸式的用户体验, 用户可以添加到桌面
当用户打开我们站点时(从桌面 icon 或者从浏览器), 通过 Service Worker 能够让用户在网络条件很差的情况下也能瞬间加载并且展现.
Service Worker 是用 JavaScript 编写的 JS 文件, 能够代理请求, 并且能够操作浏览器缓存, 通过将缓存的内容直接返回, 让请求能够瞬间完成. 开发者可以预存储关键文件, 可以淘汰过期的文件等等, 给用户提供可靠的体验.
如果站点加载时间超过 3s,53% 的用户会放弃等待. 页面展现之后, 用户期望有平滑的体验, 过渡动画和快速响应.
为了保证首屏的加载, 我们需要从设计上考虑, 在内容请求完成之前, 可以优先保证 App Shell 的渲染, 做到和 Native App 一样的体验, App Shell 是 PWA 界面展现所需的最小资源.
文档写得最好的, 还是百度的 lavas
Service Worker 生命周期分为这么几个状态 安装中, 安装后, 激活中, 激活后, 废弃
安装( installing ): 这个状态发生在 Service Worker 注册之后, 表示开始安装, 触发 install 事件回调指定一些静态资源进行离线缓存.
安装后( installed ):Service Worker 已经完成了安装, 并且等待其他的 Service Worker 线程被关闭.
激活( activating ): 在这个状态下没有被其他的 Service Worker 控制的客户端, 允许当前的 worker 完成安装, 并且清除了其他的 worker 以及关联缓存的旧缓存资源, 等待新的 Service Worker 线程被激活.
激活后( activated ): 在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会). 并可以处理功能性的事件 fetch (请求),sync (后台同步),push (推送).
废弃状态 ( redundant ): 这个状态表示一个 Service Worker 的生命周期结束.
Service Worker 本质, 可以看成另外一个线程启动, 做为一个中间件在发挥作用.
缓存的资源都是可以在这里看到
Service Worker 只能在 localhost 调试中或者 https 中使用, 因为它的权限过于强大, 可以拦截请求等. 所以要确保安全, 目前 PWA 并不成熟, 浏览器兼容性还是不那么好, 但是它用起来是真的很舒服
另外一种持久化存储方案:
localStorage
浏览器 API 有 localStorage.getItem 等...
有类似将 JS 文件缓存写入 localStorage 然后通过与服务端对比版本号再决定是否更新 JS 文件
还有在进入首页时, 将详情页的模版先存入 localStorage 当进入详情页时候直接取出, 然后发请求, 把请求回来的一小部分内容(比如图片, 渲染上去)
当然, 还有种种的应用, 骚操作.
最后是框架, 现在的单页面框架, 其实很简单. 每次更新页面, diff 对比差异后, 更新差异部分.
精细化拆分组件 , 经常变和不经常变的分拆
精细化定制数据来源, 最好做到单向数据流, 只有一个数据改变可以影响重新渲染
并不是所有的都需要在 shouldComponentUpdate 中对比然后决定是否要更新
实践证明 复用 1000 个组件渲染在页面中
用 immutable 去生成不可变数据对比
跟用 PureComponent 浅比较 后者会快很多很多
永远别忘了 JS 主线程和 GUI 渲染线程互斥.
合理手段减少重复渲染次数
如何优化你的超大型 React 应用
前端性能优化不完全手册 - 很早前写的文章
发现性能优化其实要写的太多太多, 但是, 核心点在上面和文章里了, 特别是我的那个清空渲染队列的代码, 我决定能解决很大部分的性能瓶颈.
负载均衡, Nginx 和 pm2 配置
在理解 Nginx 的用途之前先了解正向代理, 反向代理的概念:
正向代理: 是一个位于客户端和原始服务器 (origin server) 之间的服务器, 为了从原始服务器取得内容, 客户端向代理发送一个请求并指定目标(原始服务器), 然后代理向原始服务器转交请求并将获得的内容返回给客户端.
反向代理: 在计算机网络中, 反向代理是代理服务器的一种. 它根据客户端的请求, 从后端的服务器上获取资源, 然后再将这些资源返回给客户端. 与正向代理不同, 正向代理作为一个媒介将互联网上获取的资源返回给相关联的客户端, 而反向代理是在服务器端作为代理使用, 而不是客户端.
PM2 是一款非常好用的 Node.JS 服务启动容器. 它可以让你保持应用程序永远运行, 要重新加载它们无需停机(我是这么理解的: PM2 是一个监控工具).
nginx 是一款轻量化的 Web 服务器. 相较于 Apache 具有占有内存少, 并发高等优势. 使用 epoll 模型, nginx 的效率很高. 并且可以热升级.
Nginx 与 PM2 的区别:
pm2 是在应用层面单机的负载, nginx 是多用于多机集群的负载 PM2 Cluster 是对单台服务器而言的, 而 nginx 是对多台服务器而言的, 它们可以很好的结合在一起. 全篇看下来会发现, 其实 Nginx 与 PM2 完全是不一样的, 两者之间没有很大的相同点让人混淆. 换一种更容易理解的说法是: nginx 配置多站点(域名),pm2 管理 Node.JS 后台进程
使用 PM2 永动机启动 Node.JS 项目, 再使用 nginx 做反向代理, 简直完美.
因为 node.JS 程序监听的是服务器端口, 使用 nginx 做反向代理, 就可以任意配置你的二级域名来访问你的程序
这里我们主要介绍 nginx 的负载模块
HTTP 负载均衡模块(HTTP Upstream)
这个模块为后端的服务器提供简单的负载均衡 (轮询(round-robin) 和连接 IP(client IP))
如下例:
- upstream backend {
- server backend1.example.com weight=5;
- server backend2.example.com:8080;
- server unix:/tmp/backend3;
- }
- server {
- location / {
- proxy_pass http://backend;
- }
- }
Nginx 的负载均衡算法:
1.round robin(默认)
轮询方式, 依次将请求分配到各个后台服务器中, 默认的负载均衡方式.
适用于后台机器性能一致的情况.
挂掉的机器可以自动从服务列表中剔除.
2.weight
根据权重来分发请求到不同的机器中, 指定轮询几率, weight 和访问比率成正比, 用于后端服务器性能不均的情况.
例如:
- upstream bakend {
- server 192.168.0.14 weight=10;
- server 192.168.0.15 weight=10;
- }
- 3.IP_hash
根据请求者 ip 的 hash 值将请求发送到后台服务器中, 可以保证来自同一 ip 的请求被打到固定的机器上, 可以解决 session 问题.
例如:
- upstream bakend {
- ip_hash;
- server 192.168.0.14:88;
- server 192.168.0.15:80;
- }
- 4.fair
根据后台响应时间来分发请求, 响应时间短的分发的请求多.
例如:
- upstream backend {
- server server1;
- server server2;
- fair;
- }
- 5.url_hash
根据请求的 url 的 hash 值将请求分到不同的机器中, 当后台服务器为缓存的时候效率高.
例如:
在 upstream 中加入 hash 语句, server 语句中不能写入 weight 等其他的参数, hash_method 是使用的 hash 算法
- upstream backend {
- server squid1:3128;
- server squid2:3128;
- hash $request_uri;
- hash_method crc32;
- }
常见的负载均衡算法 https://www.cnblogs.com/DarrenChan/p/8967412.html
使用 pm2:
NPM install pm2 -g
pm2 start App.JS
PM2 的主要特性
内建负载均衡(使用 Node cluster 集群模块)
后台运行
0 秒停机重载, 我理解大概意思是维护升级的时候不需要停机.
具有 Ubuntu 和 CentOS 的启动脚本
停止不稳定的进程(避免无限循环)
控制台检测
提供 HTTP API
远程控制和实时的接口 API ( Node.JS 模块, 允许和 PM2 进程管理器交互 )
pm2 常用命令 https://pm2.keymetrics.io/docs/usage/quick-start/
pm2 的使用, 让我们避开了自己配置负载均衡, 守护进程等一系列. 但是高并发场景, Nginx 和内置的负载均衡, 仅仅只讲到了皮毛, 这里只是入个门.
还剩下最后三个问题, 我想写得质量高一些, 如果感觉写得不错可以点个赞, 关注下. GitHub 仓库也欢迎去 star~ 哦
来源: https://segmentfault.com/a/1190000020207259