content-visibility: العرض خارج الشاشة دون تقسيم القوائم (list virtualization)
content-visibility: العرض خارج الشاشة دون تقسيم القوائم (list virtualization)
تُعرض قائمة الألف تعليق أسفل المقال ببطء ملحوظ، ويتقطّع التمرير خلالها على هاتف أضعف أداءً. الإجابة المعتادة في React هي التقسيم الافتراضي (virtualization) – باستخدام react-window أو @tanstack/react-virtual – اللذين يعرضان في DOM فقط العناصر التي تقع فعلًا ضمن النافذة المرئية، بالإضافة إلى هامش صغير. يعمل هذا، لكن له تكلفة نادرًا ما يُتحدث عنها عند التطبيق: العناصر خارج النافذة ليست مخفية، بل غير موجودة أصلًا في DOM. الضغط على Ctrl+F لن يجد النص الموجود في التعليق رقم 850 قبل أن تُمرّر إليه يدويًا. طباعة الصفحة ستُظهر فقط ما كان معروضًا فعليًا لحظة استدعاء الطباعة. تنقّل قارئ الشاشة بين العناوين أو المعالم (landmarks) يتجاوز محتوى غير موجود فعليًا في الشجرة – وهذا ليس خللًا في التقسيم الافتراضي، بل نتيجة حتمية له.
تهاجم content-visibility مشكلة الأداء نفسها من زاوية مختلفة تمامًا: تبقى العناصر في DOM، دائمًا، كاملة. يكتفي المتصفح بتجاهل العمل الذي كان سيضطر عادة إلى تنفيذه لأجلها – التخطيط، والرسم، وإنشاء طبقات التركيب (compositor layers) – إلى أن يقترب العنصر بما يكفي من المنطقة المرئية لكي يصبح هذا العمل ذا معنى.
ماذا يعني بالضبط «تجاهل العمل»
إن content-visibility: auto على عنصر ما تُلزم المتصفح بتطبيق احتواء CSS (CSS Containment) عليه – وهي الآلية نفسها التي سمحت، في مقال استعلامات الحاوية، بتجنب الحلقة اللانهائية عند الاستعلام عن حجم الحاوية. هنا يخدم الاحتواء غرضًا مختلفًا: إذا كان العنصر خارج الشاشة، يستطيع المتصفح أن يفترض بأمان أن تخطيطه الداخلي ورسمه لا يؤثران في أي شيء خارج حدوده، لذا يمكنه تأجيل هذا العمل بالكامل، دون حساب بكسل واحد في داخله.
css
.comment {content-visibility: auto;contain-intrinsic-size: auto 180px;}
لا تُعدّ contain-intrinsic-size إضافة تجميلية – فبدونها سينكمش العنصر المتجاهَل إلى ارتفاع صفري، لأن المتصفح لا يملك مصدرًا لمعرفة حجمه ما دام لا يحسب التخطيط في داخله. هذه المشكلة نفسها بالضبط التي واجهناها مع container-type: size دون ارتفاع صريح – إلا أن النتيجة هنا ليست بطاقة تختفي، بل شريط تمرير يقفز، لأن ارتفاع المستند يتغيّر فجأة كلّما دخل تعليق آخر في وضع التجاهل أو خرج منه. تخبر القيمة auto 180px المتصفح بأمرين في آنٍ واحد: استخدم 180 بكسلًا كتقدير أوّلي، قبل أن يُعرض العنصر فعليًا ولو مرة واحدة، ثم احتفظ بحجمه الحقيقي المحسوب واستخدم هذه القيمة المحفوظة كعنصر نائب في كل مرة يُتجاهل فيها لاحقًا – فإذا كانت التعليقات الحقيقية تختلف في ارتفاعها، يصبح تخمين المتصفح لمقدار المساحة الواجب حجزها أدق تدريجيًا مع الوقت، بدلًا من التمسك برقم واحد ثابت.
حيلة لا تحصل عليها مع التقسيم الافتراضي الكامل: «مخفي حتى يُعثر عليه» (hidden until found)
هذا هو الموضع الذي تفعل فيه content-visibility شيئًا لا يستطيع التقسيم الافتراضي فعله من حيث المبدأ – لا من الناحية العملية فقط. العنصر في وضع auto، رغم تجاهله من ناحية العرض، لا يزال يحتفظ بنصه فعليًا داخل DOM. يعرف المتصفح ذلك ويتعامل مع هذا العنصر باعتباره «مخفيًا لكن قابلًا للمطابقة» (hidden but matchable): فعندما يضغط المستخدم Ctrl+F ويكتب عبارة تطابق نصًا داخل عنصر متجاهَل، يتراجع المتصفح مؤقتًا عن التجاهل، ويعرض العنصر فعليًا، ويُبرز التطابق، ويُمرّر إليه – تلقائيًا، دون أي كود من جانبك. لم يفعل display: none هذا قط (يتجاهله Ctrl+F ببساطة)، ولا تستطيع القائمة المُقسَّمة افتراضيًا فعل ذلك، لأن النص الذي تبحث عنه غير موجود أصلًا في DOM قبل أن تُمرّر إليه يدويًا.
مثال عملي: قائمة تعليقات دون سطر واحد من جافاسكريبت للتقسيم الافتراضي
jsx
const CommentList = ({ comments }) => (<ul className="comment-list">{comments.map((comment) => (<li key={comment.id} className="comment"><p className="comment__author">{comment.author}</p><p className="comment__body">{comment.body}</p></li>))}</ul>);export default CommentList;
css
.comment {content-visibility: auto;contain-intrinsic-size: auto 180px;padding: 12px 0;border-bottom: 1px solid #e5e5e5;}
يعرض المكوّن بأكمله كل عناصر <li> الألف داخل DOM – دون أي منطق لنافذة التمرير، ودون أي حساب لتحديد العناصر المرئية حاليًا، ودون أي مكتبة. يقرر المتصفح بنفسه أيّ التعليقات من بين الألف يستحق حساب التخطيط والرسم له في لحظة معينة، وأيّها «غير موجود» من الناحية البصرية البحتة، رغم وجودها كاملة في DOM.
متى لا يكفي هذا – ولماذا لا يزال التقسيم الافتراضي منطقيًا
تحلّ content-visibility تكلفة العرض – التخطيط والرسم. لكنها لا تحلّ التكلفة التي غالبًا ما تؤلم أكثر في تطبيق React: كل عنصر من عناصر <li> الألف لا يزال مكوّن React حقيقيًا يُركَّب (mount)، ويُرطَّب (hydrate)، وقد تكون له خطافاته (hooks) وتأثيراته (effects) الخاصة. تجاهل العرض على مستوى CSS لا يجعل هذه الآلاف من نسخ المكوّنات تتوقف عن الوجود في شجرة React، ولا يوقف تكلفة جافاسكريبت اللازمة لإنشائها. بالنسبة إلى كتل المحتوى البسيطة والثابتة في معظمها (التعليقات، فقرات مقال طويل، صفوف جدول بلا تفاعلية)، هذا غير مهم – فالتكلفة الحقيقية كانت أصلًا في التخطيط/الرسم، لا في منطق المكوّن. أما بالنسبة إلى القوائم المكوَّنة من مكوّنات ثقيلة وتفاعلية – لكل منها حالته واشتراكاته وتكلفة عرض باهظة – فيبقى التقسيم الافتراضي الحقيقي، الذي يزيل النسخ غير المستخدمة من شجرة React بالكامل، الوسيلة الوحيدة أحيانًا للحفاظ على السلاسة.
المزالق والممارسات الجيدة
contain-intrinsic-sizeإلزامية عمليًا، لا اختيارية. إغفالها يؤدي إلى قفزات في التمرير وارتفاع خاطئ للمستند عند كل انتقال للعنصر إلى وضع التجاهل والعودة منه.content-visibility: hiddenليست مثلauto. تتجاهلhiddenالعرض دائمًا، بغض النظر عن الموضع على الشاشة، لكن على عكسdisplay: none، فإنها تحتفظ بحالة التخطيط المحسوبة داخليًا في ذاكرة مخبأة – فإعادة إظهار العنصر (عند تبديل تبويب مثلًا) تكون أرخص مما هي عليه بعدdisplay: none، لأن المتصفح لا يحسب كل شيء من الصفر. كما يُزال العنصر ذوcontent-visibility: hiddenبشكل صحيح من شجرة إتاحة الوصول، تمامًا مثلdisplay: none– وهذا خيار آمن للتبويبات واللوحات التي تُبدَّل كثيرًا، لا مجرد حيلة أداء.- ليست بديلًا عن التقسيم الافتراضي للقوائم ذات المكوّنات الثقيلة. اعتبرها الأداة الأولى الأرخص للقوائم البسيطة والثابتة في معظمها – ولا تلجأ إلى التقسيم الافتراضي الحقيقي إلا عندما يُظهر المُحلِّل (profiler) فعليًا تكلفة على جانب جافاسكريبت/React، لا في التخطيط وحده.
- تحقق من الدعم قبل الاعتماد الكامل على «مخفي لكن قابل للمطابقة». تتمتع
content-visibility: autoوحدها اليوم بدعم واسع في جميع المحركات الرئيسية، لكن سلوك «الكشف المؤقت» عند استخدام Ctrl+F غالبًا ما يكون أكثر نضجًا في محركات Chromium – تعامل معه كإضافة لطيفة، لا كضمان تبني عليه متطلبًا من متطلبات إتاحة الوصول.
خلاصة
يبدو أن التقسيم الافتراضي وخاصية content-visibility يحلّان المشكلة نفسها للوهلة الأولى – قائمة طويلة تُعرض ببطء شديد – لكنهما يفعلان ذلك على مستويين مختلفين تمامًا. يزيل التقسيم الافتراضي العناصر من DOM ومن شجرة React بالكامل، ويدفع ثمن ذلك خسارة Ctrl+F والطباعة وجزء من تنقّل إتاحة الوصول. أما content-visibility فتترك DOM كاملًا وتكتفي بجعل المتصفح يتجاهل عمل العرض المكلف لما هو غير مرئي حاليًا فقط – في المقابل تحصل على تحسين أقل عدوانية، لكنه مجاني، دون كسر أي شيء كان المتصفح قادرًا أصلًا على فعله مع المستند الكامل. بالنسبة إلى معظم القوائم الطويلة والبسيطة نسبيًا، هذه هي الأداة الأولى الصحيحة – ويبقى التقسيم الافتراضي احتياطيًا للحالات التي لا يكفي فيها هذا فعليًا.