正則表達式在咱們日程的工做項目中,應該是一個常常用到的技能。在作一些字符的匹配和處理的過程當中,發揮了很大的做用。咱們這篇文章主要是經過一個我在工做中遇到的性能問題,來探究下正則表達式是如何影響咱們的代碼性能的。在咱們遇到了正則表達式有性能平靜的時候,咱們應該如何的來對它進行優化?html
若是對正則表達式尚未什麼概念,或者說不了解的同窗,能夠先參考我以前寫過的博客:前端
在咱們平常的工做中,若是不須要去調整正則表達式的話,大部分人實際上是會選擇性忽略它的。這就致使了大部分人對正則表達式其實並非太瞭解。在正則表達式出現問題之後也不知道如何去解決。正則表達式
由於我在美團是負責作大象Web/PC的相關開發,因此在平常的工做中免不了要常常和正則表達式打交道,好比識別文本消息中的URL進行高亮,或者說識別會議室、解析特定格式展現不一樣的UI等。在這種狀況下,我免不了會跟大量的正則表達式打交道。從長時間與正則打交道的經歷中,也有了部分的經驗總結。typescript
下面咱們經過一個工做中具體的例子,來看下正則表達式是如何讓你的網頁卡住的?segmentfault
在最近的性能問題優化排查中,咱們發如今遇到文字內容較多(約15000字)的文本消息文字處理時,render函數會有一個比較大的性能損耗,每次渲染須要差很少100ms。由於消息每次渲染都是20條一塊兒,所以正則表達式一旦有性能問題,就會由於屢次渲染的放大效應,被用戶很明顯的感知到。若是每條消息處理都須要100ms,那麼20條消息處理就會直接卡頓2s,這其實對於用戶來講是不能夠接受的。後端
具體咱們能夠看下火焰圖(火焰圖就是Chrome的devtools中,分析profile時候的圖表,你們能夠理解爲一個調用時間圖譜,若是不瞭解,推薦看看阮一峯老師的如何讀懂火焰圖? - 阮一峯的網絡日誌):網絡
經過上述的火焰圖,咱們能夠看到這個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):
這個和你們的猜想是否是同樣?在我以前最先的猜想中,我也覺得是正則長度越長,那麼性能就越差。可是,這個和個人猜想正好相反,反卻是看上去比較短的。引用正在表達式性能問題最大。
從咱們分析的數據來看,在10000字以前,其實差異沒有那麼大。 可是在超過10,000個字的時候,其實耗時差別就比較明顯了。
你們能夠看到引用的這個正則表達式,他的耗時實際上是發生了指數型的上升。 在超過50,000字,之後其實這個正則你能夠認爲基本上就不可以再使用了,並且這仍是在性能比較好的MacBook狀況下。 若是是在一些更老的電腦,或者說Windows的低端本上,那麼這個耗時其實還會更大。你想一想你,你可以接受你的開發的項目,卡住2秒不動嗎?
反卻是咱們以爲比較複雜的這個會議室正則表達式,它在匹配的內容字數增長的狀況下,性能其實沒有明顯的增長,一直都穩定在100毫秒如下。
看到這裏,有人可能會以爲是否是match方法,它比較吃性能呢?也有人可能會想,咱們是否是在match以前增長一個相同正則表達式的test判斷?若是符合的話,咱們再執行match,這樣是否是就可以提升咱們的性能呢?
那麼咱們把match方法換成test方法來看一下,這樣能不可以提高咱們正則匹配的性能呢?下圖是咱們使用會議室正則表達式來進行匹配的一個耗時圖。咱們從圖中能夠看到相關的執行耗時狀況:
從圖中能夠看到,test方法並不會比match方法節省更多的時間,相反來看他的耗時其實比match還略微有增長。不過可能就是幾個毫秒。我嘗試了一下性能問題更明顯的引用正則表達式,獲得告終論也是同樣的。因此咱們想到的先使用test方法來進行判斷,若是test方法命中的話再進行match。這個不但沒有優化,反卻是可能會損耗雙倍的性能。
既然相同的正則表達式使用任意一個方法執行的時候都會有比較明顯的性能問題,那麼咱們就只能從正則表達式自己的優化入手了。咱們來看一下,爲何咱們以爲比較複雜的正則表達式,耗時沒有什麼變化。反而咱們認爲比較簡單的正則表達式時間的增加卻這麼明顯呢?
其實,正則表達式性能最大的影響來自於正則表達式的回溯。若是一個正則表達式回溯的越多,那麼它的性能損耗就越明顯。咱們能夠去看一下上面兩個正則表達式的狀況。
其實上面兩個正則表達式都有回溯的問題。若是你們不瞭解,回溯,能夠去看下我以前的那一篇 正則表達式高級進階。在這裏咱們簡單介紹一下回溯回溯的緣由:正則表達式在匹配的過程當中須要往回走從新進行匹配,這就會致使回溯。通常產生回溯的有這麼幾種狀況,一種是分支,一種是量詞。
咱們能夠看看上面兩個正則表達式,會議是這個正則比較簡單,他實際上是不少分支的集合體;引用的這個正則就不一樣了,他的回溯主要是來源於量詞。尤爲是[^「]*
這種的存在,致使了大量的回溯狀況。
因此說一個正則表達式性能好很差跟他的長短沒有必然的聯繫。而是跟他具體的寫法有關。若是這個正則表達式不少地方都有回溯的狀況,那麼他的性能必然就好不了。反過來講,若是一個正則表達式雖然很長很複雜,可是它可以儘量的避免回溯。須要匹配的文本也儘量的清晰,那麼這種狀況下它的性能實際上是很不錯的。
遇到這個問題,咱們通常會有如下兩個解決方案。
第一個解決方案就是儘量的去優化這個正則表達式自己,去儘量消除裏面一些回溯的狀況。這個也是咱們通常最經常使用的一個解決方案。具體有如下2個操做:
\d{1, 30}
來替換.*
,儘量的去明確咱們須要匹配的類型與長度。同時,還有個規則:在不須要捕獲組的狀況下,括號儘量的使用非捕獲組(與回溯無)。
整體上來講就是:若是一個正則表達式越精確,捕獲的元素越少,那麼它的性能就會越好。反之,若是有大量的模糊匹配跟回溯的狀況,那麼它的性能大機率就不怎麼好。
在通常的場景中,咱們使用了這個方法,基本上咱們的性能問題就可以迎刃而解了。
可是,那麼若是咱們繼續要匹配比較複雜的正則,同時這個正則又沒有辦法避免回溯的狀況,咱們應該怎麼去優化這個性能的?
也就是說在這種狀況下,這個正則表達式實際上是沒有辦法再進行優化了,可是咱們又須要在平常的項目中使用,不能直接廢棄。這就須要咱們使用另外的優化方案了。
在正則沒有辦法修改的狀況下,咱們能夠作正則匹配的分級,儘量使用一些性能比較高的正則表達式,先進行一些過濾匹配。在命中咱們須要匹配的條件之後,再使用比較複雜的正則表達式進行匹配。從而避免複雜的正則表達式頻繁的被調用。
我舉一個簡單的例子,仍是以上面的引用正則表達式來分析。若是這個正則表達式我沒有辦法再進行進一步優化了狀況下,咱們能夠先把他的一些特定的規則摘取出來,進行一個前置校驗。咱們能夠簡單的來看一下下面一個代碼示例:
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服務如何可以彈性擴容了。
在個人項目遇到的性能問題中,只使用了前兩個方案對引用的正則表達式進行了優化。咱們能夠來看一下優化後的渲染耗時狀況:
在經過對正則表達式進行優化後,咱們的每次文本渲染時間從100ms直接降到了不到2ms。 這但是50倍的性能提高。對於15000字的文原本說,這個速度能夠算是沒有任何的性能影響了。
咱們還試了試極限狀況下1000000字的狀況,渲染也可以控制在20ms之內,這和以前相比,進步仍是很明顯的。
正則表達式在咱們的平常代碼使用中其是很常見的。可是稍有不慎咱們就會遇到性能問題。大部分在寫代碼的過程當中,不會去考慮這個正則表達式性能怎麼樣,都會下意識以爲反正處理的文本長度不大,寫的再差也沒有什麼影響。可是,在項目逐漸發展過程當中,有可能因爲產品策略調整或者數據的積累,某一個不起眼的正則表達式,就會對整個項目的性能產生決定性影響。
所以咱們在具體開發的過程當中必定要有性能的意識,咱們寫的任意一個正則表達式都有可能會致使整個系統的性能問題。所以咱們寫的每個正則表達式都應該儘量的準確,儘量的減小執行次數。
再遇到正則的性能問題時,正則表達式的優化手段主要有3個:
但願可以經過上述的具體實戰優化,可以讓你們瞭解正則表達式在項目中對性能的影響,也歡迎你們在遇到正則表達式相關的問題時,隨時討論交流,你們一塊兒解決問題,一塊兒進步。