不知不觉有三个月没有写文章了,这期间逐渐熟悉了新公司,比如git flow,比如项目的技术栈,技术上也是有一定的成长的,但是觉得还是需要产出,只有记录下自己的成长,回头看才会觉得踏实。工作中其实有很多小细节可以挖掘,研究一下或许就是小的突破,不断的积累,相信一年后会大不一样。
初步分析
这篇文章用于记录从一次生产上的异常分析底层的原因,最后得到解决方案的过程。
起因是业务发现,有一个pdf文件打开预览后会导致浏览器卡死,但其他pdf文件都可以成功预览,是相对难以处理的个例问题。
特殊问题特殊处理,于是先分析pdf文件本身,发现没有什么异常,可以通过本地的pdf软件成功展示,说明pdf本身并无问题。
然后分析事故的表象。选中pdf文件后,浏览器卡死,点击页面无反应,但是其他标签页正常运转(chrome是每个标签页一个独立的进程,所以一个标签页卡死不会影响其他的)。然后打开f12,想利用f12的内存检测来分析是否发生了内存泄漏。操作过程是:开启f12内存监控-->选中文件-->文件解析-->卡死
因为卡死导致没有拿到内存监控的结果,期间往f12中的console输入命令也没有反应,经过非常长的一段时间后,pdf加载出来了,终于拿到了f12的内存监控,之前往console中敲入的命令也如雨后春笋冒了出来,这种感觉就像是网络不好的时候连续发了一大堆的请求,忽然网络正常了,全部请求都得到返回一样。
接着分析内存监控的数据,发现这个pdf只有三页,大小也只有440K,但是解析用了127M的内存,相比之下,一个16页700k大的pdf,解析用到的内存是72m。初步认为,是解析和渲染pdf的时候出现了问题,由于某种原因导致pdf的解析和渲染开销变得很大。
从代码入手
接着分析前端代码,发现展示的部分是使用react-pdf这个库来完成渲染的,于是拉取库的源码,看看是不是库底层的问题。拉取后发现源码是打包发布的,换言之就是类似react、vue的源码一样,并不是直接可以运行到浏览器的项目,这个项目对外暴露的只是几个接口,外部通过这些接口来解析pdf和渲染pdf。从而判断该项目需要先把源码打包,在react工程中添加依赖后引入使用的。
本地部署
demo github: github.com/OJAccepted/…
分析了一下react-pdf的源码的package.json,发现打包过程并不复杂,只是使用babel把源码打包到了dist文件夹下,对外暴露的是entry.js这个文件。于是判断源码本身可以作为接口使用。用create-react-app创建了一个react的应用,
1)直接将react-pdf的源码拷贝到src目录下
2)把react-pdf中package.json中的dependencies添加到项目中的package.json、运行npm install
3)将app.jsx修改为react-pdf中sample的sample.jsx
4)将sample.less改成sample.css放在我们的src下
5)修改了一下entry.js中workerspace的指向,并且在Document.jsx中添加一句pdfjs-dist/build/pdf.worker.entry.js
运行项目,便可以本地调试react-pdf的源码了。(其实还有一个方法,就是把react-pdf的源码拷贝到本地,然后通过npm link把源码和项目链接起来,这样就可以运行项目并且调试其中引入的第三方库了)
pdf-js的入口文件是entry.js这个文件的作用仅仅是对外暴露pdfjs(Mozilla解析pdf的库),Document,Outline,Page,而我用到的只是Document和Page,稍微看了看Document和Page的入口文件,其中Document的作用是展示整个pdf文件,而page则是pdf中的每一页,换言之,Document里面包含了很多页Page,就好比一个pdf里面有很多页。然后实际运行项目,试图通过下断点分析原因。
经过前面的分析,初步认为是pdf的解析出现了问题,于是调试的第一步是在解析相关的地方下断点,看看是哪一步花费了大量的时间。由于react-pdf对外暴露的主要接口有两个:Document和Page,于是在下面几个地方下了断点:
Document.jsx loadDocument方法 findDocumentSource方法 renderChildren方法
Page.jsx loadPage方法 renderChildren方法
utils.js loadFromFile方法
调试时发现,utils的loadFromFile是可以成功读取到pdf的内容的
接着调试,发现代码在Page.jsx的renderChildren方法里面调用了一个叫renderTextLayer的方法,该方法中对于pdf中的每个字母都要渲染一个span标签,因为pdf中一页就有一万多个字母,react-pdf会为这些字母都渲染一个span标签。众所周知,dom操作的开销是非常大的,而且dom操作会占用浏览器的主线程,因此当然会出现卡死的情况了。
下面有两张图,展示的是渲染性能的差别。图一中是渲染一万个包含a这个字符的span标签,而图二中是渲染一个包含一万个a字符的span标签,可以看出两者之间的性能差异多么大。
(渲染一万个包含a这个字符的span标签)
(渲染一个包含一万个a字符的span标签)
回溯原因,是由于/Page/TextLayer.jsx 中有一个loadTextItems方法,这个方法中调用pdf api解析拿到的文本内容是一个字符数组,文本中的每个字母都是一个独立的项。而正常的pdf中,一个文本段落的字母是合起来一个项的,可以比对一下下面两张图的区别
那么问题很明显了:根因是pdf内是逐个逐个字母存储字母的,浏览器为这些字母渲染文本导致了卡死。因此是pdf本身和底层库共同导致的bug,与我们应用的代码无关,可是既然出了bug就得修。
解决方案
升级库
首先的解决方案是升级库,由于项目是从别的组接过来,原有的库基本都比较落后,因此希望升级库可以修复这个bug。
首先更新了一下package.json,把库升级到^5.1.0,并且在用到react-pdf的地方加一行pdfjs.GlobalWorkerOptions.workerSrc = //cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js;
升级后bug依然存在,并未解决
阻止文本渲染
既然是文本渲染导致的 bug,那么有木有办法阻止呢?通过阅读react-pdf的api,发现在Page中有一个属性renderTextLayer可以控制文本的渲染。(原本pdf的展示并不需要这个文本层的渲染,这里文本层的用处是方便用户可以选中文本,进行复制粘贴等操作,经与产品经理讨论,可以不要这个功能,于是关闭文本渲染,bug修复了)
重新实现pdf预览组件
据leader所说,pdf是可以借据浏览器本身的能力进行渲染的,只需要加一个iframe即可,但是我们的pdf还需要翻页、缩放等功能,如果自己实现无疑是一个大工程,因此没有采用这个方案。