Skip to content

内存泄漏

线上的商城打开某个商品会出现页面卡死,页面没有报错,所以可能是出现了内存泄露,我打开devtool 里的performance monitor,有两项数据异常,一个是cpu占用,一个是js堆,

堆大小不断增加直到页面崩溃。我怀疑是出现了死循环。我尝试在本地复现,但是无法复现,我看页面也没有报错,但是同事电脑上有,应该是依赖问题,所以我更新了依赖。最后还是不行,后面发现跟依赖预构建有关

报错信息大概跟el-select组件相关,我在这个组件相关的函数里进行打印,发现会打印100次。这跟vue源码有关。

但是内存泄露的原因出在el-select上,我准备了一个最小复现,运行elementplus源码,在里面所有的watch进行打印,发现有个watch执行了100遍。于是就定位到了,之后提了一个pr

大文件切片

首先对文件进行切片,传入一个file对象,使用slice对文件按指定大小进行切割获得切片数组。

针对文件生成唯一标识,使用spark-md5(/spa:rk/)生成hash。考虑到这一步将占用大量资源,阻塞主线程。因此在web worker中进行这个操作

调用初始化上传接口,请求参数是切片数,文件hash。返回taskId

调用切片上传接口,入参切片序号,taskId,请求体里文件切片(formData),返回成功,更新上传进度

但是浏览器限制并发请求的个数,如果使用promise.all一次性的并发请求太多可能导致请求直接失败,使用p-limit帮助我们解决限制并发个数的问题

优化:

秒传,在初始化上传接口中如果后端根据hash发现文件已经存在,则会返回一个特殊的状态码表示该文件已存在

断点上传,调用初始化上传接口,如果没有上传完,返回缺失的切片序号,前端再补充上传缺失的切片即可

暂停上传,axios提供的AbortController取消上传

使用切片的原因

  • 提高上传稳定性,当网络出现波动这个上传可能失败,通过断点续传,我们不需要重新上传整个文件
  • 不管是客户端和服务端的内存消耗都会减少
  • 通过小块上传,可以充分利用带宽,提升上传速度
  • 可以提供更精确的上传进度反馈

webworker传递大数据

主线程与webwork传递的数据是复制的,而不是共享,需要经过序列号与反序列化,大部分浏览器使用结构化克隆算法(structured clone algorithm)来实现。

但是可转移对象( Transferable object)通过零拷贝操作从一个上下文转移到另一个上下文,这在发送大型数据集时可带来巨大的性能提升。

这些资源可以从一个上下文转移到另一个,但是资源一次只能在一个上下文可用

vscode插件

我开发的时候遇到一个让我感觉麻烦的问题,当我使用一些公用的hook,它导出的东西比较多,我不得不去其他文件里复制再粘贴回来,这种切换上下文的操作让我感到非常不舒服。

我的思路是把公用的hooks和utils放在侧边栏下,点击导出的函数即可自动插入到当前光标的位置,我认为这对于每个项目都是通用的。想实现这个效果我必须借助vscode插件

因为我是从0开始开发的,所以需要走搭建框架,调试,开发,发布到插件市场整个流程,每个部分我都需要研究学习文档并且还需要学习node的部分api。当然报错是必不可少的,所以解决报错也花了很多时间。

完成这个功能我需要获取文件里导出的内容,实现这个功能的技术路线也十分曲折,最终确定使用babel的ast来获取导出内容,并且ast可以增强我们插件的能力,即便如此使用ast解析除导出内容也不是一样容易的是,所以针对这个功能还写了一个npm包

ui方面有两种实现分别是使用treeview或者webview,最终选择使用treeView

除了之外呢想做的更通用,通过vscode setting可以自由配置,例如现在默认所有的hooks是放在src下的hooks文件夹下,通过配置,不是叫hooks也行

还有最后一个优化点,修改导出的内容,插件不会有感知,所以内容还是旧的。我看了其他插件是带了一个refresh的按钮,我也实现了这个功能

并且通过vscodeapi可以实现实时监听不需要手动刷新,对性能也不会造成较大影响。使用zustand做全局状态管理

ast节点查询优化

  • 避免多次遍历,尽量在一次遍历收集全部信息,如果找到了节点,可以提前跳出对其子树的遍历
  • 使用babel内置的路径查询api并对查询条件过滤
  • 对已解析的文件建立缓存,如果没有修改就没必要重新解析
  • 对高频查询节点创建索引
  • 使用swc代替babel解析ast

ast原理

  • 首先进行词法分析,将源代码拆分成最小语法单元token,通过有限状态机生成token序列
  • 进行语法分析,根据编程语言的语法规则,将Tokens组合成AST,语法分析器会检验代码是否符合语法规范

组件库

瀑布流

描述

这个瀑布流不太一样,他有搜索功能,瀑布流下的商品列表里附带sku信息,点击不同的sku展示的图片也会发生变化,也就是高度会发生变化,并且我想做成懒加载和响应式的。也就是根据屏幕会变化列数

说一下实现思路,因为后面优化会基于这个

首先对于瀑布流要先拿到它的高度,开始我想让后端返回高宽比,但是后端没时间弄,后来我自己看华为云obs文档,发现可以通过api的方式获取图片信息.对于图片懒加载,是使用了vue的自定义指令以及web api intersectionobserve.最后是关于响应式.这里我使用到web api的resizeobserve,我会根据宽度更新应该展示多少列,然后再把元素重新分配到这些列中

性能优化

  1. 首先是图片方面,为了降低FCP,LCP
  • 图片格式可以转成webp格式,按照华为云obs文档,在url上添加请求参数可以返回webp格式图片,但是这么请求的图片不会被http缓存,并且每请求一次都会重新进行格式转化,成本会大幅上升。所有需要上传的时候就将图片转成webp格式存放在桶里,返回的时候返回webp即可
  • 按照需求控制图片请求大小,虽然我这里是做的响应式布局,但是我会以第一次计算的列宽作为图片宽度请求,但会回到之前那个问题也就是不会被http缓存,这样虽然可以加速首屏,但是之后每次都需要重新获取。我是通过service worker来解决的。我会拦截指定的图片域名请求,如果没有命中会放入cache中,命中则从cache中取。当然也可以给缓存设置过期时间,类似于强缓存的效果
  • 可以使用preconnect提前建立与obs域名的连接,当然如果需要与多个第三方域建立连接,全部preconnect可能会适得其反,可以替换为dns-prefetch
  • 我观察到现在的协议是http1.1,可以使用http2解决队头阻塞的问题,当然http2也只是解决了http层的阻塞,tcp层的阻塞没有解决,http3就是为了解决tcp层的队头阻塞问题,他使用的是quic协议,基于utp而非tcp,所有下一代方案是使用http3
  • 可以使用cdn,打包后的资源使用Gzip或br压缩

2.代码或者框架层面

  • 我刚才又提到我自己fetch api拿到图片信息,之前是在for循环了一个一个await拿到,但是可以并发发送请求.因为每个网站都有最大连接数,需要写一个限制最大并发数的函数,并发数我设置的是6.
  • 刚才提到我实现了一个懒加载的自定义指令,每个图片里都会new 一个intersectionobserve,我想使用一个单例实现,并且我在mdn看了文档,发现它可以observe某个元素,不需要可以unobserve.证明这是可行的,所以我封装了一个hook用来创建intersectionobserve实例.这样只需要一个实例既可完成监听功能
  • 对于resizeObserve,因为他的触发频率会很高,所以使用了节流,并且将回调放在requestAnimationFrame里,还有就是我只关心它的宽度,如果宽度不变,高度改变,不会触发回调,这在初始化的时候高度会变化
  • 框架上则是使用computed缓存了一些计算结果,有使用到watch,但是这个watch只是第一次有用,使用了vue一个比较新的配置项once.来实现一次监听即销毁
  • 最后在umounted生命周期释放之前那两个web api实例的内存占用

用户体验上

  1. ui上的要求是滚动时隐藏搜索条件,使用了vue的内置组件transion实现了v-if的动画。
  2. 对于瀑布流组件需要先获取数据才能渲染,为了视觉不会太突兀,这里准备了一个骨架屏

性能指标 整体得分是从93到96,fcp提升12.5%,lcp提升18.75%,speed index提升了27.27%,在低速网络下因为骨架屏会造成一些cls偏移

本来想加个虚拟列表的功能,但是因为子元素的高度是会变化的,重新计算高度应该不会造成性能问题,有性能问题也可以放到web worker里执行,但是因为虚拟列表部分dom不会渲染,刚才我那个自定义指令因为没有渲染请求下一页好像不会执行,这个问题我还在研究.