ביטויים רגולריים הם כלי עוצמתי לעיבוד טקסט מורכב, אבל ידעתם שהם עלולים להאט את הביצועים של האפליקציה שלכם באופן דרמטי? הפעם נגלה את החולשות של Regex, ונראה איך אפשר להשיג שיפור ביצועים על ידי שימוש באלטרנטיבות מהירות יותר, פשוטות יותר וקריאות יותר.
דמיינו שיש לכם כמות גדולה של פסקאות, כאשר כל אחת מהן מכילה 20,000 תווים. כל פסקה מתחילה ב-foo
ומסתיימת ב-bar
. אם תרצו לבדוק שכל פסקה עומדת במבנה הזה, כנראה תכתבו משהו כזה:

במבט ראשון, זה עובד מצוין, אפילו עם 10,000 פסקאות:

אבל מה קורה כשמגדילים את הכמות? אם, למשל, נגדיל למיליון פסקאות, נגלה שהבדיקה הופכת להיות איטית מאוד:

איך אפשר לבצע שיפור ביצועים?
האם עלינו להמתין כל כך הרבה זמן עד שהבדיקה תסתיים? ממש לא. אפשר לפתור את זה בקלות עם הקוד הבא:

התוצאה? אפילו אם נריץ את זה על 50 מיליון (!) פסקאות, זה כמעט לא יורגש:

אז למה Regex איטי כאן?
כדי להבין למה ה-Regex הופך לאיטי כל כך, נצטרך להבין מה קורה מאחורי הקלעים. הביטוי הרגולרי שבנינו קודם מפעיל שלוש בדיקות נפרדות:
^foo
– בודק שהטקסט מתחיל ב-foo
.bar$
– בודק שהטקסט מסתיים ב-bar
..*
עם הדגלs
– מבטיח שכל תו ביןfoo
ל-bar
הוא… ובכן, תו כלשהו 😂 וזה בדיוק החלק הבעייתי!
ה-.*
מכריח את מנוע ה-Regex לעבור על כל תו בטקסט, למרות שאנחנו יודעים שאין בזה שום צורך. זה גורם לסיבוכיות של O(n)
, מה שהופך להיות צוואר הבקבוק מבחינת הביצועים.
מהצד השני, startsWith
ו-endsWith
לא דורשים מעבר על כל המחרוזת, אלא מבצעים בדיקה ישירה על ההתחלה והסוף בלבד ומספקים לנו יעילות מקסימלית.
האם חייבים לוותר על Regex בשביל שיפור ביצועים?
זה אולי נראה ש-startsWith
ו-endsWith
הם בסך הכל "sugar syntax" לבדיקה עם Regex, כי אפשר לבצע בדיקה זהה עם הקוד הבא:

אבל, למרות שהם נראים זהים, בדיקה פשוטה תגלה שכשמריצים את זה 50 מיליון פעמים, זה איטי כמעט פי 5!

לכן, המסקנה הבלתי נמנעת היא ש-Regex
לא נועד לספק את הביצועים הטובים ביותר, וכשמדובר בכמויות גדולות של נתונים או פעולות, עדיף לשקול אלטרנטיבות כמו זאת שראינו.
וחוץ מזה, בינינו – פונקציות כמו startsWith
ו-endsWith
עושות את הקוד להרבה יותר קריא ואלגנטי, לא? 😉
דוגמה נוספת: שיפור ביצועים של חיפוש בקצוות הטקסט
בואו נראה עוד דוגמה ליעילות של פונקציות חלופיות: נניח שאנחנו רוצים לבדוק אם foo
מופיע איפשהו בתוך 12 התווים הראשונים, ו-bar
איפשהו בתוך 12 התווים האחרונים. עם Regex, הקוד ייראה בערך ככה:

ושוב, תוצאות לא טובות:

הפעם, נראה ש-startsWith
ו-endsWith
לא יכולים לעזור לנו, כי אנחנו לא מחפשים משהו שמופיע בדיוק בתחילת או בסוף המחרוזת. למרות זאת, אנחנו יכולים להשתמש בפונקציות אחרות כדי לשפר את הקריאות ובעיקר – להאיץ את המהירות באופן משמעותי:

ותראו איך גם כשאנחנו מריצים את זה 100 מיליון פעמים, זה פשוט עובד בצורה חלקה:

היינו יכולים לחשוב ששימוש ב-substring
, שיוצר בפועל מערך חדש בכל איטרציה, הוא יקר יותר מאשר Regex
, אבל מסתבר שלא – הפעולה הזאת זולה בהרבה ותיתן לנו את אותה בדיקה בדיוק, עם כמעט אפס Overhead בביצוע!
(כמובן, יכולנו לממש את החיפוש בעצמנו באמצעות לולאה שעוברת על המחרוזת עד למיקום 12 במקום להשתמש ב-substring
, אבל השיפור המזערי שהיינו מקבלים כאן לא שווה את הקוד המורכב והקריא-פחות שהיינו צריכים לתחזק).
יש עוד דוגמאות?
ברור! הנה כמה דוגמאות נוספות שממחישות איך שימוש בפונקציות מובנות של JavaScript יכול לחסוך זמן ריצה יקר. נסו להריץ אותן ותראו עד כמה ההבדל משמעותי כשזה מגיע לכמויות גדולות:

או למשל:

סיכום
כמו תמיד, פיצ'רים מובנים בשפה הם עוצמתיים, אבל חשוב להבין איך להשתמש בהם באופן נכון ויעיל. שימוש לא זהיר או בלי הבנה מעמיקה מספיק עלול לגרום לבעיות ביצועים משמעותיות. על ידי הבנה של מה שקורה מאחורי הקלעים, אפשר לכתוב קוד מהיר, נקי ויעיל בהרבה.
Regex
הוא כלי חזק – תשתמשו בו בחוכמה! 🚀
רוצה עוד טיפים לאופטימיזציה של ביצועי JavaScript? תעקבו אחרי המאמרים הנוספים בסדרה! 🚀
👈 לקריאת המאמר הקודם בסדרה על שיפור ביצועים בעבודה עם פורמטינג של תאריכים, מטבעות ואחוזים.
מאמרים קשורים
The Dark Side of JS Formatting
Are you aware of this Regex pitfall
Think One WebWorker is Enough? Think Again