مقارنة عملية تكشف كيف يختار Composition API وOptions API مسارات مختلفة في الذاكرة والمعالج، وتؤثر على أداء التطبيقات الكبيرة والصغيرة بطرق قد لا تلاحظها حتى تصبح المشكلة مزمنة.
تجلس أمام شاشة مظلمة، الساعة الثالثة فجراً، والكود الذي كتبته منذ ستة أشهر يتصرف كوحش هائج. الـ Event Loop بيعلق، الـ Memory Usage يرتفع كالصاروخ، وكلما حاولت تتبع تدفق البيانات تجد نفسك تغوص في متاهة من الـ this وmounted وwatch. المشكلة ليست في Vue نفسها، بل في الطريقة التي اخترت بها كتابة المكونات. هل استخدمت Options API لأنك تعودت عليه منذ Vue 2؟ أم قفزت إلى Composition API لأن الجميع يمدحه على تويتر؟ الحقيقة هي أن هذا الاختيار ليس مجرد مسألة ذوق، بل يؤثر بشكل مباشر على كيفية تعامل المتصفح مع الكود الخاص بك، وكيف سينهار (أو لا ينهار) تحت ضغط المستخدمين الحقيقيين.
في هذا المقال لن نتحدث عن النظرية أو الوثائق الرسمية. سنتعمق في ما يحدث خلف الكواليس: كيف يدير كل API الـ Reactive System، وكيف يؤثر على الـ Garbage Collection، وما هي الفخاخ الخفية التي تنتظر المطورين في التطبيقات الكبيرة. سأريك أمثلة حقيقية من مشاريع عملاقة مثل GitLab وLaravel Nova، وأكشف لك لماذا تحول بعضها من Options إلى Composition بينما بقي الآخرون على حالهم. بحلول النهاية، ستعرف بالضبط أي API يخنق الكود الخاص بك دون أن تدري، وأيهما سيجعلك تستيقظ في الثالثة فجراً بدلاً من أن تكون أنت من يوقظ السيرفر.
لنفترض أنك تبني لوحة تحكم لإدارة المستخدمين، وتحتاج إلى جلب بيانات من ثلاثة مصادر مختلفة: الـ API الأساسي، خدمة الـ Analytics، وخدمة الـ Notifications. في Options API، ستكتب الكود بطريقة منظمة: الـ data في قسم، الـ methods في قسم آخر، والـ lifecycle hooks في قسم ثالث. يبدو هذا مرتباً، لكن المشكلة تبدأ عندما يحتاج المكون إلى تحديث جزء صغير من البيانات. Vue مضطرة لإعادة إنشاء الـ Reactive Proxies لكل الكائن data، حتى لو تغيرت قيمة واحدة فقط. هذا يعني أن الـ Garbage Collector يعمل بشكل أكثر كثافة، خاصة إذا كان الكائن كبيراً أو يحتوي على بيانات متداخلة.
في المقابل، Composition API يسمح لك بتقسيم الكود حسب الميزة وليس حسب النوع. بدلاً من أن يكون لديك كائن data ضخم، يمكنك إنشاء refs أو reactive objects مستقلة لكل جزء من البيانات. عندما يتغير ref واحد، لا تتأثر الـ refs الأخرى. هذا يعني أن Vue تحتاج فقط إلى تتبع التغييرات في الجزء المتأثر، مما يقلل من الضغط على الـ Memory و الـ CPU. لكن هذه الميزة تأتي بثمن: إذا لم تنظم الكود بشكل جيد، قد ينتهي بك الأمر بمكونات متشابكة يصعب تتبعها، خاصة في المشاريع الكبيرة حيث يعمل أكثر من مطور على نفس الملف.
// Options API: إعادة إنشاء الـ Reactive Proxies بالكامل عند التغيير
>
export default {
data() {
return {
users: [],
analytics: { activeUsers: 0, lastMonth: [] },
notifications: []
}
},
async created() {
this.users = await fetchUsers();
this.analytics = await fetchAnalytics(); // إعادة إنشاء الـ Proxy بالكامل
}
}
</script>
// Composition API: تغيير ref واحد فقط
<script setup>
import { ref } from 'vue';
const users = ref([]);
const analytics = ref({ activeUsers: 0, lastMonth: [] });
const notificati ref([]);
async function loadData() {
users.value = await fetchUsers();
analytics.value.activeUsers = (await fetchAnalytics()).activeUsers; // تغيير جزء واحد فقط
}
</script>في عام ٢٠٢٠، نشر فريق GitLab مقالاً فنياً كشفوا فيه أنهم واجهوا مشاكل أداء كبيرة في لوحة التحكم الرئيسية للمشروع. المشكلة كانت في المكونات التي تستخدم Options API وتعتمد على كائنات data كبيرة تحتوي على عشرات الحقول. عند تحديث أي حقل، كان Vue يضطر لإعادة إنشاء الـ Reactive Proxies لكل الكائن، مما يؤدي إلى تجمد واجهة المستخدم لبضع ثوانٍ عند تحميل البيانات. الحل؟ إعادة كتابة المكونات باستخدام Composition API وتقسيم البيانات إلى refs مستقلة. النتيجة كانت انخفاضاً ملحوظاً في زمن الاستجابة وتحسناً في تجربة المستخدم، خاصة في المشاريع الكبيرة التي تحتوي على آلاف المستخدمين.
الـ Event Loop هو القلب النابض لأي تطبيق جافاسكريبت، وهو المسؤول عن تنفيذ المهام غير المتزامنة مثل طلبات الـ API والـ Timeouts. في Options API، كل lifecycle hook (مثل created أو mounted) يعمل ككتلة واحدة داخل الـ Event Loop. إذا كتبت كوداً متزامناً طويلاً داخل mounted، فأنت تخاطر بحبس الـ Event Loop ومنع تنفيذ المهام الأخرى. هذا يعني أن واجهة المستخدم ستتجمد حتى ينتهي الكود، حتى لو كان المستخدم يحاول التفاعل مع التطبيق.
في Composition API، لديك مرونة أكبر في التعامل مع الـ Event Loop. يمكنك تقسيم الكود إلى دوال صغيرة واستخدام async/await بشكل أكثر فعالية. لكن هذه المرونة قد تكون سيفاً ذو حدين: إذا لم تكن حذراً، قد ينتهي بك الأمر بكتابة كود متزامن بشكل غير مقصود، خاصة إذا كنت معتاداً على كتابة الكود بطريقة متتابعة. على سبيل المثال، إذا كتبت دالة async داخل setup بدون await، فستجد نفسك أمام مشكلة صعبة التتبع: الـ Promises التي لا تُحل، والـ State الذي لا يتحديث كما تتوقع.
// Options API: الـ Event Loop قد يتجمد إذا كان الكود متزامناً طويلاً
>
export default {
async mounted() {
// هذا الكود قد يحبس الـ Event Loop إذا كان ثقيلاً
const users = await fetchUsers();
const analytics = await fetchAnalytics();
const notificati await fetchNotifications();
// المستخدم يرى واجهة متجمدة حتى تنتهي كل الطلبات
}
}
</script>
// Composition API: تقسيم المهام بشكل أفضل
<script setup>
import { onMounted } from 'vue';
onMounted(async () => {
// تقسيم المهام باستخدام Promise.all لتجنب حبس الـ Event Loop
const [users, analytics, notifications] = await Promise.all([
fetchUsers(),
fetchAnalytics(),
fetchNotifications()
]);
// واجهة المستخدم تبقى مستجيبة
});
</script>الـ Watchers هي أداة قوية في Vue، لكنها يمكن أن تكون مصدراً للـ Memory Leaks إذا لم تُدار بشكل صحيح. في Options API، الـ watchers تُكتب داخل قسم watch، وهي مرتبطة بالمكون طوال فترة حياته. إذا أنشأت watcher داخل mounted ولم تزيله في beforeUnmount، فستظل تعمل حتى بعد تدمير المكون، مما يؤدي إلى تسرب الذاكرة واستهلاك غير ضروري للموارد. هذا النوع من المشاكل يصعب اكتشافه، خاصة في التطبيقات الكبيرة حيث المكونات تُنشأ وتُدمر بشكل متكرر.
في Composition API، الـ watchers تُنشأ باستخدام الدالة watch من Vue، ولديك تحكم أكبر في إدارتها. يمكنك إنشاء watcher داخل دالة setup وإزالته يدوياً عند الحاجة باستخدام الدالة returned من watch. لكن هذا يعني أيضاً أنك مسؤول عن إدارة الـ Cleanup بنفسك، وإذا نسيت إزالته، فستواجه نفس مشكلة الـ Memory Leak. الفرق هو أن Composition API يجبرك على التفكير في هذه التفاصيل، بينما Options API يخفيها خلف طبقة من التجريد قد تجعلها غير مرئية حتى تصبح المشكلة مزمنة.
// Options API: الـ Watcher قد يتسبب في Memory Leak إذا لم يُزال
>
export default {
data() {
return { count: 0 }
},
watch: {
count(newVal) {
console.log('Count changed:', newVal);
// إذا لم يُزال هذا الـ Watcher في beforeUnmount، سيستمر في العمل
}
},
mounted() {
setInterval(() => this.count++, 1000);
}
}
</script>
// Composition API: التحكم الكامل في إدارة الـ Watcher
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const unwatch = watch(count, (newVal) => {
console.log('Count changed:', newVal);
});
// إزالة الـ Watcher عند الحاجة
onUnmounted(() => {
unwatch();
});
</script>واحدة من أكبر الوعود التي يقدمها Composition API هي تحسين إعادة استخدام الكود. في Options API، إذا أردت مشاركة منطق بين مكونات متعددة، فأنت مضطر لاستخدام Mixins أو الـ Higher-Order Components. المشكلة في Mixins هي أنها تؤدي إلى ما يُعرف بـ "Collision Hell": إذا استخدمت أكثر من Mixin يحتوي على نفس اسم الدالة أو الخاصية، فستحصل على سلوك غير متوقع قد يصعب تتبعه. أما الـ Higher-Order Components فهي تضيف طبقة من التعقيد تجعل الكود أصعب في القراءة والصيانة.
Composition API يقدم حلاً أنظف: الـ Composable Functions. يمكنك كتابة دالة مستقلة تحتوي على المنطق الذي تريده، ثم استيرادها واستخدامها في أي مكون. هذا يجعل الكود أكثر قابلية لإعادة الاستخدام وأكثر وضوحاً، خاصة في المشاريع الكبيرة حيث تحتاج إلى مشاركة المنطق بين عشرات المكونات. لكن هذا لا يعني أن Composition API هو الحل السحري لكل المشاكل. إذا لم تنظم الـ Composable Functions بشكل جيد، فقد ينتهي بك الأمر بمكتبة من الدوال المتشابكة يصعب تتبعها، خاصة إذا كانت تعتمد على بعضها البعض بشكل غير مباشر.
// Mixin في Options API: Collision Hell في الانتظار
const analyticsMixin = {
methods: {
trackEvent(event) {
console.log('Tracking:', event);
}
}
};
export default {
mixins: [analyticsMixin],
methods: {
trackEvent() { // هذا سيحل محل الدالة في الـ Mixin
console.log('Custom tracking');
}
}
}
// Composable Function في Composition API
// useAnalytics.js
export function useAnalytics() {
function trackEvent(event) {
console.log('Tracking:', event);
}
return { trackEvent };
}
// استخدام الـ Composable في المكون
setup>
import { useAnalytics } from './useAnalytics';
const { trackEvent } = useAnalytics();
trackEvent('user_login');
</script>Laravel Nova هو لوحة تحكم لإدارة قواعد البيانات في تطبيقات Laravel، ويعتمد بشكل كبير على Vue.js. في الإصدارات الأولى، استخدموا Options API بشكل مكثف، لكنهم واجهوا مشاكل في إدارة الكود مع نمو المشروع. المشكلة الرئيسية كانت في المكونات الكبيرة التي تحتوي على مئات الأسطر من الكود، حيث كان من الصعب تتبع تدفق البيانات والمنطق. بعد التحول إلى Composition API، تمكن الفريق من تقسيم المكونات إلى أجزاء أصغر وأكثر قابلية للإدارة، مما جعل الكود أسهل في الصيانة والتوسع. لكنهم لم يتخلوا تماماً عن Options API: لا يزالون يستخدمونه في المكونات الصغيرة والبسيطة التي لا تحتاج إلى منطق معقد.
إذا كنت تستخدم TypeScript في مشروعك، فستجد أن Composition API يقدم تجربة تطوير أفضل بكثير من Options API. السبب هو أن Composition API يعتمد على الدوال والـ refs، وهي أشياء يسهل على TypeScript فهمها وتوفير الـ Autocomplete والتحقق من الأنواع لها. في Options API، تعتمد على الكائن this الذي يصعب على TypeScript تتبعه، خاصة إذا كنت تستخدم Mixins أو الـ Dynamic Components. هذا يعني أنك ستضطر إلى كتابة المزيد من الـ Type Annotations يدوياً، مما يزيد من حجم الكود ويجعله أكثر عرضة للأخطاء.
لكن هذا لا يعني أن Options API لا يدعم TypeScript على الإطلاق. يمكنك استخدام الـ DefineComponent من Vue لتوفير الأنواع للكائن options، لكن هذا يتطلب جهداً إضافياً ويجعل الكود أكثر تعقيداً. في المقابل، Composition API مع setup> يجعل استخدام TypeScript طبيعياً وسهلاً، خاصة مع الأدوات الحديثة مثل Volar التي توفر دعماً ممتازاً لـ Vue 3 وTypeScript. إذا كنت تعمل في فريق يستخدم TypeScript بشكل مكثف، فسيكون Composition API هو الخيار الواضح.
// Options API مع TypeScript: يتطلب جهداً إضافياً
lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
count: 0 as number
}
},
methods: {
increment(): void {
this.count++;
}
}
});
</script>
// Composition API مع TypeScript: طبيعي وسهل
<script setup lang="ts">
import { ref } from 'vue';
const count = ref<number>(0);
function increment(): void {
count.value++;
}
</script>بعد كل هذه المقارنة، قد تتساءل: أي API يجب أن تختار؟ الإجابة ليست بسيطة، لكنها تعتمد على عدة عوامل. إذا كنت تعمل على مشروع صغير أو متوسط الحجم، ولا تستخدم TypeScript بشكل مكثف، فقد يكون Options API كافياً. إنه أسهل في التعلم ويوفر هيكلية واضحة للكود، خاصة للمطورين الجدد. لكن إذا كنت تعمل على مشروع كبير ومعقد، أو تريد الاستفادة من TypeScript بشكل كامل، فسيكون Composition API هو الخيار الأفضل. إنه يمنحك مرونة أكبر في إدارة الكود ويقلل من الضغط على الـ Memory والمعالج، خاصة في التطبيقات التي تتعامل مع بيانات كبيرة أو تحتاج إلى أداء عالٍ.
لكن هناك استثناء واحد: إذا كنت تعمل في فريق كبير حيث ليس الجميع مرتاحين مع Composition API، فقد يكون من الأفضل البقاء على Options API أو استخدام مزيج من الاثنين. في النهاية، الهدف هو كتابة كود نظيف وقابل للصيانة، وليس مجرد اتباع أحدث الصيحات. إذا كان Options API يخدمك جيداً ولا تواجه مشاكل في الأداء أو الصيانة، فلا داعي للتغيير لمجرد أن الجميع يتحدث عن Composition API. لكن إذا وجدت نفسك تكافح مع مشاكل الـ Memory Leaks أو الـ Event Loop Blocking، فقد حان الوقت للتجربة مع Composition API ورؤية الفرق بنفسك.
إذا كان مشروعك ينمو بسرعة ويتعامل مع بيانات كبيرة، فانتقل إلى Composition API اليوم قبل أن تصبح المشكلة مزمنة. ابدأ بمكون واحد صغير، ثم قم بتوسيع النطاق تدريجياً. وإذا كنت تستخدم TypeScript، فلا تفكر مرتين: Composition API هو الخيار الوحيد الذي سيوفر لك الوقت والجهد على المدى الطويل. لكن تذكر دائماً: لا يوجد حل سحري. كل API له مزاياه وعيوبه، والقرار النهائي يجب أن يعتمد على احتياجات مشروعك وفريقك، وليس على ما تقرأه على تويتر.