正则表达式是如何让你的网页卡住的

概述

正则表达式在咱们日程的工做项目中,应该是一个常常用到的技能。在作一些字符的匹配和处理的过程当中,发挥了很大的做用。咱们这篇文章主要是经过一个我在工做中遇到的性能问题,来探究下正则表达式是如何影响咱们的代码性能的。在咱们遇到了正则表达式有性能平静的时候,咱们应该如何的来对它进行优化?html

若是对正则表达式尚未什么概念,或者说不了解的同窗,能够先参考我以前写过的博客:前端

问题现状

在咱们平常的工做中,若是不须要去调整正则表达式的话,大部分人实际上是会选择性忽略它的。这就致使了大部分人对正则表达式其实并非太了解。在正则表达式出现问题之后也不知道如何去解决。正则表达式

由于我在美团是负责作大象Web/PC的相关开发,因此在平常的工做中免不了要常常和正则表达式打交道,好比识别文本消息中的URL进行高亮,或者说识别会议室、解析特定格式展现不一样的UI等。在这种状况下,我免不了会跟大量的正则表达式打交道。从长时间与正则打交道的经历中,也有了部分的经验总结。typescript

下面咱们经过一个工做中具体的例子,来看下正则表达式是如何让你的网页卡住的?segmentfault

在最近的性能问题优化排查中,咱们发如今遇到文字内容较多(约15000字)的文本消息文字处理时,render函数会有一个比较大的性能损耗,每次渲染须要差很少100ms。由于消息每次渲染都是20条一块儿,所以正则表达式一旦有性能问题,就会由于屡次渲染的放大效应,被用户很明显的感知到。若是每条消息处理都须要100ms,那么20条消息处理就会直接卡顿2s,这其实对于用户来讲是不能够接受的。后端

具体咱们能够看下火焰图(火焰图就是Chrome的devtools中,分析profile时候的图表,你们能够理解为一个调用时间图谱,若是不了解,推荐看看阮一峰老师的如何读懂火焰图? - 阮一峰的网络日志):
image.png网络

经过上述的火焰图,咱们能够看到这个render渲染函数每次执行都差很少100ms。对于JavaScript来讲,100ms其实时间已经很长了。那么这一百毫秒中具体干了哪些事情呢?函数

咱们简单的梳理一下当前的代码,发现最有可能的缘由就是正则耗时的影响。在消息处理中,有两个须要进行匹配的正则,一个是匹配会议室进行高亮的,一个是匹配引用消息进行格式转换的。这两个正则分别以下:post

const QUOTED_MSG_REG = /([^「]*?)「((?:[a-zA-Z0-9\u4E00-\u9FBF_\.\s]{0,40})\:(?:.|\n)*)」\n(—){10}\n((?:\S|\s)*)$/m;

const MEETING_ROOM_REG = /北京厅|天津厅|石家庄厅|济南厅|哈尔滨厅|...(此处省略200+个会议室)|台湾厅/mg;

这个两个正则表达式用来匹配的文本以下:性能

// 引用格式
「张三:老司机」
——————————
带带我

// 会议室
张三呀,咱们去 常德厅 开个会吧,叫上其余人

一开始看,你们可能以为这两个正则都很正常,咱们在正常的工做中也会写出这样的正则表达式,没有发现什么问题。

若是告诉你这两个正则表达式执行有性能问题,那么你们可能还会以为,会议室匹配的文本正则这么长,须要匹配的会议室这么多,确定是这个正则有性能问题,致使了执行时间过长。

那么具体状况究竟是不是和咱们直观感觉同样呢?咱们来对具体问题进行一个分析。

问题分析

为了分析咱们上面说到的这两个正则表达式性能到底怎么样,我从网上找了一些文字,来模拟消息的内容。经过使用正则表达式进行匹配,在Node端执行计算耗时,获得的一个字数与时间的关系图以下,表格的横坐标是字数,纵坐标是时间(ms):
image.png

这个和你们的猜想是否是同样?在我以前最先的猜想中,我也觉得是正则长度越长,那么性能就越差。可是,这个和个人猜想正好相反,反却是看上去比较短的。引用正在表达式性能问题最大。

从咱们分析的数据来看,在10000字以前,其实差异没有那么大。 可是在超过10,000个字的时候,其实耗时差别就比较明显了。

你们能够看到引用的这个正则表达式,他的耗时实际上是发生了指数型的上升。 在超过50,000字,之后其实这个正则你能够认为基本上就不可以再使用了,并且这仍是在性能比较好的MacBook状况下。 若是是在一些更老的电脑,或者说Windows的低端本上,那么这个耗时其实还会更大。你想一想你,你可以接受你的开发的项目,卡住2秒不动吗?

反却是咱们以为比较复杂的这个会议室正则表达式,它在匹配的内容字数增长的状况下,性能其实没有明显的增长,一直都稳定在100毫秒如下。

看到这里,有人可能会以为是否是match方法,它比较吃性能呢?也有人可能会想,咱们是否是在match以前增长一个相同正则表达式的test判断?若是符合的话,咱们再执行match,这样是否是就可以提升咱们的性能呢?

那么咱们把match方法换成test方法来看一下,这样能不可以提高咱们正则匹配的性能呢?下图是咱们使用会议室正则表达式来进行匹配的一个耗时图。咱们从图中能够看到相关的执行耗时状况:
image.png

从图中能够看到,test方法并不会比match方法节省更多的时间,相反来看他的耗时其实比match还略微有增长。不过可能就是几个毫秒。我尝试了一下性能问题更明显的引用正则表达式,获得告终论也是同样的。因此咱们想到的先使用test方法来进行判断,若是test方法命中的话再进行match。这个不但没有优化,反却是可能会损耗双倍的性能。

既然相同的正则表达式使用任意一个方法执行的时候都会有比较明显的性能问题,那么咱们就只能从正则表达式自己的优化入手了。咱们来看一下,为何咱们以为比较复杂的正则表达式,耗时没有什么变化。反而咱们认为比较简单的正则表达式时间的增加却这么明显呢?

原理分析

其实,正则表达式性能最大的影响来自于正则表达式的回溯。若是一个正则表达式回溯的越多,那么它的性能损耗就越明显。咱们能够去看一下上面两个正则表达式的状况。

其实上面两个正则表达式都有回溯的问题。若是你们不了解,回溯,能够去看下我以前的那一篇 正则表达式高级进阶。在这里咱们简单介绍一下回溯回溯的缘由:正则表达式在匹配的过程当中须要往回走从新进行匹配,这就会致使回溯。通常产生回溯的有这么几种状况,一种是分支,一种是量词。

咱们能够看看上面两个正则表达式,会议是这个正则比较简单,他实际上是不少分支的集合体;引用的这个正则就不一样了,他的回溯主要是来源于量词。尤为是[^「]*这种的存在,致使了大量的回溯状况。

因此说一个正则表达式性能好很差跟他的长短没有必然的联系。而是跟他具体的写法有关。若是这个正则表达式不少地方都有回溯的状况,那么他的性能必然就好不了。反过来讲,若是一个正则表达式虽然很长很复杂,可是它可以尽量的避免回溯。须要匹配的文本也尽量的清晰,那么这种状况下它的性能实际上是很不错的。

解决方案

遇到这个问题,咱们通常会有如下两个解决方案。

优化正则表达式自己

第一个解决方案就是尽量的去优化这个正则表达式自己,去尽量消除里面一些回溯的状况。这个也是咱们通常最经常使用的一个解决方案。具体有如下2个操做:

  1. 在明确匹配规则的状况下,使用\d{1, 30}来替换.*,尽量的去明确咱们须要匹配的类型与长度。
  2. 在须要进行不明确数量匹配的时候,尽量的使用非贪婪匹配,而不是使用贪婪匹配。

同时,还有个规则:在不须要捕获组的状况下,括号尽量的使用非捕获组(与回溯无)。

整体上来讲就是:若是一个正则表达式越精确,捕获的元素越少,那么它的性能就会越好。反之,若是有大量的模糊匹配跟回溯的状况,那么它的性能大几率就不怎么好。

在通常的场景中,咱们使用了这个方法,基本上咱们的性能问题就可以迎刃而解了。

可是,那么若是咱们继续要匹配比较复杂的正则,同时这个正则又没有办法避免回溯的状况,咱们应该怎么去优化这个性能的?

优化正则表达式匹配顺序

也就是说在这种状况下,这个正则表达式实际上是没有办法再进行优化了,可是咱们又须要在平常的项目中使用,不能直接废弃。这就须要咱们使用另外的优化方案了。

在正则没有办法修改的状况下,咱们能够作正则匹配的分级,尽量使用一些性能比较高的正则表达式,先进行一些过滤匹配。在命中咱们须要匹配的条件之后,再使用比较复杂的正则表达式进行匹配。从而避免复杂的正则表达式频繁的被调用。

我举一个简单的例子,仍是以上面的引用正则表达式来分析。若是这个正则表达式我没有办法再进行进一步优化了状况下,咱们能够先把他的一些特定的规则摘取出来,进行一个前置校验。咱们能够简单的来看一下下面一个代码示例:

let str = 'xxxxxx'; //长文本

const LINE_REG = /\n(—){10}\n/m;
const QUOTED_MSG_REG = /([^「]*?)「((?:[a-zA-Z0-9\u4E00-\u9FBF_\.\s]{0,40})\:(?:.|\n)*)」\n(—){10}\n((?:\S|\s)*)$/m;

if(LINE_GER.test(str)) {
    let result = str.match(QUOTED_MSG_REG);
    // do something
}

不要在主线程中执行

若是一个正则表达式没有办法经过上述两种方案进行优化(这个几率其实已经很低了,感受和彩票中奖差很少了),那么咱们还有一个最终的解决方案,就是使用Web Workder,来进行耗时的操做计算。

这样的话,咱们至少在主线程执行过程当中,不会有卡住影响用户操做的问题。

不过,在这个方案中,须要考虑到大量数据经过postMessage传递到Web Worker中的性能损耗问题。

这个方案本质上比较简单,我在具体项目中也没有使用到,所以不展开讲了,有兴趣了解的同窗能够自行上网查阅相关资料,或者评论私信留言讨论。

从上面的代码中咱们能够看到,咱们能够选取一个没有回溯的明确特征条件来先进行一次快速的匹配。通常状况来讲没有回溯的正则匹配效率都是特别高,即便是在大量文本处理的状况下也不会对性能有什么太大的影响。在进行了第一次正则表达式匹配后,若是这个文本仍是符合当前的条件,那么说明有较大几率它实际上是须要咱们命中的,那么咱们再执行正则匹配便可。

这样的话,咱们就可以避免大部分的无心义的性能消耗。

服务端数据处理

若是一个数据量太过庞大(超过1M的文本)时,我推荐对数据进行分页,不要一次性处理全部数据(这个时候正则已经不是瓶颈了,JS执行引擎才是瓶颈)。

可是,有些神奇的项目就是会有这种诉求,遇到这种状况时,咱们必须(不是能够,是必须)借助服务端来进行数据处理,前端只作简单的展现逻辑(即便是展现这么大量的数据,渲染也会有比较明显的卡顿和耗时)。

若是没有后端的支持,那么本身用Node搭建一个简单的中转处理服务都行。这个时候须要关注的,就是本身的Node服务如何可以弹性扩容了。

效果验证

在个人项目遇到的性能问题中,只使用了前两个方案对引用的正则表达式进行了优化。咱们能够来看一下优化后的渲染耗时状况:
image.png

在经过对正则表达式进行优化后,咱们的每次文本渲染时间从100ms直接降到了不到2ms。 这但是50倍的性能提高。对于15000字的文原本说,这个速度能够算是没有任何的性能影响了。

咱们还试了试极限状况下1000000字的状况,渲染也可以控制在20ms之内,这和以前相比,进步仍是很明显的。

总结

正则表达式在咱们的平常代码使用中其是很常见的。可是稍有不慎咱们就会遇到性能问题。大部分在写代码的过程当中,不会去考虑这个正则表达式性能怎么样,都会下意识以为反正处理的文本长度不大,写的再差也没有什么影响。可是,在项目逐渐发展过程当中,有可能因为产品策略调整或者数据的积累,某一个不起眼的正则表达式,就会对整个项目的性能产生决定性影响。

所以咱们在具体开发的过程当中必定要有性能的意识,咱们写的任意一个正则表达式都有可能会致使整个系统的性能问题。所以咱们写的每个正则表达式都应该尽量的准确,尽量的减小执行次数。

再遇到正则的性能问题时,正则表达式的优化手段主要有3个:

  1. 咱们须要尽量的去让咱们的正则表达式准确化,越准确的正则表达式匹配时,他的回溯状况就越少,因此它的性能就越高。
  2. 在正则表达式已经没有办法再进行优化的状况下,咱们能够先选取一些没有回复状况的特征值进行先置条件判断,这样的话,咱们可以尽可能多的去避免一些无心义的好事匹配,优化咱们的性能。
  3. 借助其余线程或者服务来进行正则处理,避免用户卡顿。

但愿可以经过上述的具体实战优化,可以让你们了解正则表达式在项目中对性能的影响,也欢迎你们在遇到正则表达式相关的问题时,随时讨论交流,你们一块儿解决问题,一块儿进步。

相关文章
相关标签/搜索