إرسال ٣ ميجابايت من JavaScript لمتصفح المستخدم ليس مجرد بطء، بل جريمة ضد تجربة المستخدم. في هذا الدليل العملي، سنبني تطبيق Vue 3 متكاملاً من الصفر للإنتاج، مع التركيز على الأداء والأمان والبنية القابلة للتوسع دون الوقوع في فخاخ الـ Over-Engineering.
في المثال السابق، استخدام `computed` بدلاً من `v-if` مع `v-for` يقلل من عدد عمليات إعادة الرسم بشكل كبير. لماذا؟ لأن `computed` يحفظ النتيجة ويعيد حسابها فقط عندما تتغير البيانات المعتمدة عليها، بينما `v-if` مع `v-for` يعيد تقييم الشرط في كل دورة من الـ Event Loop. هذا الفرق قد يبدو بسيطاً، لكنه يصبح كارثياً عندما يكون لديك قائمة تحتوي على مئات العناصر.
تحميل جميع مكونات التطبيق عند بدء التشغيل هو خطأ شائع آخر. بدلاً من ذلك، يجب تحميل المكونات فقط عندما يحتاجها المستخدم. في Vue Router، يمكنك القيام بذلك بسهولة باستخدام `() => import()`. مثلاً:
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue')
},
{
path: '/settings',
name: 'Settings',
component: () => import(/* webpackChunkName: "settings" */ '@/views/Settings.vue')
}
];في هذا المثال، المكونات `Dashboard.vue` و `Settings.vue` لن تُحمّل إلا عندما يزور المستخدم المسار `/dashboard` أو `/settings`. هذا يقلل من حجم الحزمة الأولية بشكل كبير. لكن هناك مشكلة شائعة هنا: إذا كان لديك مكونات مشتركة بين عدة مسارات، مثل مكون `Header` أو `Sidebar`، فقد ينتهي بك الأمر بتكرار الكود في عدة ملفات. الحل هو استخدام `webpackChunkName` نفسه للمكونات المشتركة، بحيث يتم تحميلها مرة واحدة فقط.
إذا كان تطبيقك يحتوي على قوائم طويلة، مثل قائمة المنتجات أو الرسائل، فإن استخدام `v-for` بشكل مباشر سيؤدي إلى بطء شديد في المتصفح. الحل هو استخدام تقنية الـ Virtual Scrolling، التي تقوم بعرض العناصر المرئية فقط على الشاشة وتحميل الباقي عند التمرير. هناك مكتبات جاهزة مثل `vue-virtual-scroller`، لكن يمكنك أيضاً بناء حل بسيط بنفسك:
<template>
<div class="virtual-list" @scroll="handleScroll">
<div class="virtual-list-container" :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-list-item"
:style="{ transform: `translateY(${item.top}px)` }"
>
{{ item.name }}
</div>
</div>
</div>
</template>
setup>
import { ref, computed, onMounted } from 'vue';
const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` })));
const itemHeight = 50;
const visibleCount = ref(10);
const scrollTop = ref(0);
const totalHeight = computed(() => items.value.length * itemHeight);
const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight));
const visibleItems = computed(() => {
const endIndex = Math.min(startIndex.value + visibleCount.value, items.value.length);
return items.value.slice(startIndex.value, endIndex).map((item, index) => ({
...item,
top: (startIndex.value + index) * itemHeight
}));
});
function handleScroll(e) {
scrollTop.value = e.target.scrollTop;
}
</script>
<style>
.virtual-list {
height: 500px;
overflow-y: auto;
border: 1px solid #ccc;
}
.virtual-list-container {
position: relative;
}
.virtual-list-item {
position: absolute;
width: 100%;
height: 50px;
box-sizing: border-box;
border-bottom: 1px solid #eee;
}
</style>في هذا المثال، بدلاً من عرض ١٠٠٠ عنصر في نفس الوقت، نقوم بعرض ١٠ عناصر فقط (يمكن تعديل العدد حسب الحاجة). عند التمرير، نقوم بتحديث `scrollTop` وإعادة حساب العناصر المرئية. هذا يقلل من عدد العناصر في الـ DOM بشكل كبير، مما يحسن الأداء بشكل ملحوظ. لاحظ أن هذا الحل بسيط ولا يدعم جميع الحالات، مثل القوائم ذات الارتفاعات المتغيرة، لكنه جيد كنقطة بداية.
Vuex كان الخيار الأساسي لإدارة الحالة في Vue 2، لكن مع Vue 3، أصبح Pinia هو الخيار الموصى به. الفرق الرئيسي بين Vuex وPinia هو أن Pinia أبسط وأكثر مرونة، لكنه يتطلب أيضاً فهماً أعمق لكيفية عمل الـ Reactivity في Vue. أحد الأخطاء الشائعة التي أراها هو تخزين بيانات غير تفاعلية في الـ Store، مثل الكائنات الكبيرة أو البيانات التي لا تتغير. مثلاً:
// خطأ: تخزين بيانات غير تفاعلية في Store
import { defineStore } from 'pinia';
export const useC defineStore('config', {
state: () => ({
config: {
apiUrl: 'https://api.example.com',
maxItems: 100,
theme: 'light'
}
})
});في هذا المثال، الكائن `config` لا يتغير أبداً، لذلك ليس هناك حاجة لتخزينه في الـ Store. بدلاً من ذلك، يمكنك تخزينه في ملف ثابت واستيراده عند الحاجة. هذا يقلل من حجم الـ Store ويحسن الأداء. قاعدة عامة: إذا كانت البيانات لا تتغير أو لا تحتاج إلى تفاعل، فلا تضعها في الـ Store.
في Pinia، الـ Actions هي المكان المناسب لوضع المنطق المعقد، مثل جلب البيانات من الـ API أو معالجة البيانات. لكن الكثير من المطورين يضعون كل شيء في الـ Actions، حتى العمليات البسيطة التي يمكن تنفيذها مباشرة في المكونات. هذا خطأ لأن الـ Actions تُنشئ طبقة إضافية من التعقيد دون داعٍ. مثلاً:
// خطأ: وضع منطق بسيط في Action
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
}
});
// في المكون:
const store = useCounterStore();
store.increment(); // غير ضروري
// الصحيح: تعديل الـ State مباشرة
const store = useCounterStore();
store.count++; // أفضلفي هذا المثال، الـ Action `increment` غير ضروري لأن العملية بسيطة ويمكن تنفيذها مباشرة. استخدم الـ Actions فقط عندما تحتاج إلى تنفيذ منطق معقد أو عمليات غير متزامنة. هذا يبقي الكود نظيفاً ويقلل من التعقيد.
الـ Getters في Pinia مشابهة للـ Computed Properties في المكونات، لكنها تُستخدم لاستخراج البيانات من الـ Store. المشكلة هي أن الـ Getters تُعيد حسابها في كل مرة يتم الوصول إليها، حتى لو لم تتغير البيانات المعتمدة عليها. لحل هذه المشكلة، يمكنك استخدام مكتبة مثل `lodash.memoize` أو بناء حل بسيط بنفسك:
import { defineStore } from 'pinia';
export const useProductStore = defineStore('product', {
state: () => ({
products: []
}),
getters: {
expensiveProducts(state) {
// إعادة الحساب في كل مرة يتم الوصول إلى الـ Getter
return state.products.filter(product => product.price > 100);
},
// Getter مع Memoization
cheapProducts: (state) => {
let cachedResult = null;
return () => {
if (!cachedResult) {
cachedResult = state.products.filter(product => product.price <= 100);
}
return cachedResult;
};
}
}
});في هذا المثال، الـ Getter `cheapProducts` يحفظ النتيجة في `cachedResult` ويعيدها في المرات التالية دون إعادة الحساب. هذا مفيد جداً إذا كان لديك عمليات حسابية معقدة أو بيانات كبيرة. لكن لاحظ أن هذا الحل بسيط ولا يدعم التحديث التلقائي عند تغير البيانات، لذلك استخدمه بحذر.
الوثائق الرسمية لـ Vue 3 تقدم نصائح جيدة لتحسين الأداء، لكنها لا تغطي كل شيء. مثلاً، هل تعلم أن استخدام `v-once` على المكونات التي لا تتغير يمكن أن يحسن الأداء بشكل كبير؟ أو أن تعطيل الـ Source Maps في الإنتاج يقلل من حجم الحزمة؟ دعنا نستعرض بعض النصائح التي لا تجدها في الوثائق:
export default defineConfig({
build: {
sourcemap: false // تعطيل Source Maps في الإنتاج
}
});إذا كنت تستخدم Vue CLI، فأنت تستخدم Webpack خلف الكواليس. وإذا كنت تستخدم Vite، فأنت تستخدم Rollup. في كلتا الحالتين، يمكنك تحسين أداء البناء باستخدام بعض الإضافات والتكوينات. مثلاً، في Webpack، يمكنك استخدام `TerserPlugin` لضغط الكود و`CompressionPlugin` لضغط الملفات باستخدام Gzip أو Brotli. في Vite، يمكنك استخدام إضافات مثل `vite-plugin-compression` لتحقيق نفس الهدف.
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
vue(),
compression({
algorithm: 'brotliCompress', // استخدام Brotli بدلاً من Gzip
ext: '.br'
})
]
});في هذا المثال، نستخدم إضافة `vite-plugin-compression` لضغط الملفات باستخدام Brotli، الذي يوفر ضغطاً أفضل من Gzip. هذا يقلل من حجم الملفات بشكل كبير، مما يحسن وقت التحميل. لكن لاحظ أن Brotli يتطلب دعماً من السيرفر، لذلك تأكد من تفعيله على السيرفر الخاص بك.
قبل نشر التطبيق للإنتاج، هناك بعض الخطوات النهائية التي يجب اتخاذها لضمان أن كل شيء يعمل بشكل مثالي. أولاً، تأكد من تشغيل التطبيق في وضع الإنتاج محلياً باستخدام:
npm run build
npm run previewهذا سيبني التطبيق في وضع الإنتاج ويعرضه محلياً باستخدام سيرفر خفيف. تحقق من أن كل شيء يعمل كما هو متوقع، خاصةً المكونات التي تعتمد على الـ Lazy Loading أو الـ Virtual Scrolling. بعد ذلك، تحقق من حجم الحزمة باستخدام أدوات مثل `webpack-bundle-analyzer` أو `rollup-plugin-visualizer`. هذه الأدوات تظهر لك حجم كل ملف ومكتبة، وتساعدك على تحديد الملفات الكبيرة التي يمكن تحسينها.
npm install -D rollup-plugin-visualizer
# ثم أضف الإضافة إلى vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
vue(),
visualizer({
open: true // فتح التقرير تلقائياً بعد البناء
})
]
});أخيراً، تأكد من تفعيل التخزين المؤقت (Caching) على السيرفر الخاص بك. استخدم رؤوس HTTP مثل `Cache-Control` و`ETag` لضمان أن المتصفح يخزن الملفات الثابتة ولا يعيد تحميلها في كل زيارة. مثلاً، يمكنك إضافة هذه التكوينات إلى ملف `.htaccess` إذا كنت تستخدم Apache:
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>هذا يخبر المتصفح بتخزين الملفات الثابتة لمدة سنة كاملة وعدم إعادة طلبها من السيرفر إلا إذا تغيرت. هذا يقلل من عدد الطلبات إلى السيرفر ويحسن وقت التحميل بشكل كبير.
بعد أكثر من عشر سنوات في بناء تطبيقات الويب، تعلمت أن الأداء ليس مجرد ميزة إضافية، بل هو أساس تجربة المستخدم. التطبيق الذي يعمل ببطء هو تطبيق فاشل، مهما كانت ميزاته رائعة. في Vue 3، المفتاح لبناء تطبيق سريع وآمن للإنتاج هو التركيز على التفاصيل الصغيرة التي لا يلاحظها معظم المطورين. مثلاً، استخدام `v-once` للمكونات الثابتة، أو تجزئة الكود باستخدام `() => import()`، أو تحسين الـ Getters في Pinia باستخدام الـ Memoization. هذه التفاصيل هي ما يميز التطبيق الجيد عن التطبيق السيئ.
نصيحة واحدة أخيرة: لا تحاول تحسين الأداء قبل أن تبني التطبيق. ابدأ ببنية بسيطة، ثم قم بقياس الأداء باستخدام أدوات مثل Lighthouse أو WebPageTest. بعد ذلك، قم بتحسين الأجزاء التي تسبب البطء فقط. هذا النهج يوفر الوقت والجهد ويضمن أن تحسن الأداء حيث يحتاجه التطبيق بالفعل. الآن، اذهب وبنِ شيئاً رائعاً!