1. قبل البدء
يعلّمك هذا الدرس التطبيقي حول الترميز كيفية إضافة ميزة التنقّل الفوري والانتقال السلس بين الصفحات إلى نموذج تطبيق ويب باستخدام أحدث واجهات برمجة التطبيقات التي يتيحها Google Chrome بشكلٍ أصلي.
يتحقّق تطبيق الويب النموذجي من القيم الغذائية للفواكه والخضروات الشائعة. تم إنشاء صفحتَي قائمة الفاكهة وتفاصيل الفاكهة كتطبيق من صفحة واحدة (SPA)، وتم إنشاء صفحتَي قائمة الخضراوات وتفاصيل الخضراوات كتطبيق تقليدي متعدد الصفحات (MPA).

على وجه التحديد، يمكنك تنفيذ العرض المُسبَق وميزة "التخزين المؤقت للصفحات" (bfcache) وخادم وكيل للتحميل المسبق الخاص من أجل التنقّل الفوري، وعمليات الانتقال بين العناصر الجذرية أو المشترَكة من أجل عمليات الانتقال السلسة بين الصفحات. يمكنك تنفيذ العرض المُسبَق وميزة "التخزين المؤقت للصفحات" لصفحات MPA، وعمليات انتقال العناصر المشتركة لصفحات تطبيق من صفحة واحدة (SPA).
لطالما كانت سرعة الموقع الإلكتروني عاملاً مهمًا في تجربة المستخدم، ولهذا السبب قدّمت Google Core Web Vitals، وهي مجموعة من المقاييس التي تقيس أداء التحميل والتفاعل والثبات البصري لصفحات الويب من أجل تقييم تجربة المستخدم الفعلية. تساعدك أحدث واجهات برمجة التطبيقات في تحسين نتيجة Core Web Vitals لموقعك الإلكتروني في المجال، لا سيما بالنسبة إلى أداء التحميل.

عرض توضيحي من Mindvalley
اعتاد المستخدمون أيضًا على استخدام الانتقالات لجعل عمليات التنقّل وتغييرات الحالة سهلة الاستخدام للغاية في تطبيقات الأجهزة الجوّالة الأصلية. مع ذلك، لا يمكن محاكاة تجارب المستخدمين هذه بسهولة على الويب. على الرغم من إمكانية تحقيق تأثيرات مشابهة باستخدام واجهات برمجة التطبيقات الحالية على الويب، قد يكون التطوير صعبًا أو معقّدًا للغاية، خاصةً عند مقارنته بميزات مماثلة في تطبيقات Android أو iOS. تم تصميم واجهات برمجة التطبيقات السلسة لسدّ هذه الفجوة في تجربة المستخدم والمطوّر بين التطبيق والموقع الإلكتروني.

عروض توضيحية من pixiv و Tokopedia
المتطلبات الأساسية
المعرفة بما يلي:
- HTML
- CSS
- JavaScript
- أدوات مطوّري برامج Chrome
ما ستتعرّف عليه:
كيفية التنفيذ:
- العرض المُسبَق
- bfcache
- خادم وكيل خاص للتحميل المسبق
- عمليات الانتقال بين العناصر الجذرية/المشترَكة
ما ستنشئه
تطبيق ويب نموذجي تم إنشاؤه باستخدام Next.js ويتضمّن أحدث إمكانات المتصفّح السريعة والسلسة:
- تنقّل شبه فوري باستخدام العرض المسبق
- ذاكرة التخزين المؤقت للصفحات (bfcache) لتحميل الصفحات على الفور باستخدام زرَّي الرجوع والتقديم في المتصفّح
- انطباعات أولية رائعة من خلال التنقّل بين المصادر باستخدام Private Prefetch Proxy أو Signed Exchange (SXG)
- انتقال سلس بين الصفحات باستخدام انتقال العناصر الجذر/المشتركة
المتطلبات
- الإصدار 101 من Chrome أو الإصدارات الأحدث
2. البدء
تفعيل ميزات Chrome التجريبية
- انتقِل إلى about://flags، ثم فعِّل علامتَي وقت التشغيل
Prerender2وdocumentTransition API. - إعادة تشغيل المتصفّح
الحصول على الشفرة
- افتح الرمز البرمجي من مستودع GitHub هذا في بيئة التطوير المفضّلة لديك:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
- ثبِّت التبعيات المطلوبة لتشغيل الخادم:
npm install
- ابدأ الخادم على المنفذ 3000:
npm run dev
- انتقِل إلى http://localhost:3000 في المتصفّح.
يمكنك الآن تعديل تطبيقك وتحسينه، فكلما أجريت تغييرات، تتم إعادة تحميل التطبيق وتظهر تغييراتك مباشرةً.
3- دمج العرض المسبق
لأغراض هذا العرض التوضيحي، تكون مدّة تحميل صفحة تفاصيل الخضروات في نموذج التطبيق بطيئة جدًا بسبب تأخير عشوائي من جهة الخادم. يمكنك إلغاء وقت الانتظار هذا من خلال العرض المسبق.
لإضافة أزرار العرض المُسبَق إلى صفحة قائمة الخضروات والسماح لها بتفعيل العرض المُسبَق بعد أن ينقر المستخدم على الزر:
- أنشئ مكوّن زر يدرج علامة النص البرمجي speculation-rules ديناميكيًا:
components/prerender-button.js
import { useContext } from 'react'
import ResourceContext from './resource-context'
// You use resource context to manage global states.
// In the PrerenderButton component, you update the prerenderURL parameter when the button is clicked.
export default function PrerenderButton() {
const { dispatch } = useContext(ResourceContext)
const handleClick = (e) => {
e.preventDefault()
e.stopPropagation()
const parent = e.target.closest('a')
if (!parent) {
return
}
const href = parent.getAttribute('href')
dispatch({ type: 'update', prerenderURL: href })
}
return (
<button className='ml-auto bg-gray-200 hover:bg-gray-300 px-4 rounded' onClick={handleClick}>
Prerender
</button>
)
}
- استورِد مكوّن
PrerenderButtonفي ملفlist-item.js.
components/list-item.js
// Codelab: Add a PrerenderButton component.
import PrerenderButton from './prerender-button'
...
function ListItemForMPA({ item, href }) {
return (
<a href={href} className='block flex items-center'>
<Icon src={item.image} />
<div className='text-xl'>{item.name}</div>
{/* Codelab: Add PrerenderButton component. */}
<PrerenderButton />
</a>
)
}
- أنشئ مكوّنًا لإضافة Speculation Rules API.
يُدرِج المكوّن SpeculationRules علامة نص برمجي ديناميكيًا في الصفحة عندما يحدّث التطبيق حالة prerenderURL.
components/speculationrules.js
import Script from 'next/script'
import { useContext, useMemo } from 'react'
import ResourceContext from './resource-context'
export default function SpeculationRules() {
const { state } = useContext(ResourceContext)
const { prerenderURL } = state
return useMemo(() => {
return (
<>
{prerenderURL && (
<Script id='speculationrules' type='speculationrules'>
{`
{
"prerender":[
{
"source": "list",
"urls": ["${prerenderURL}"]
}
]
}
`}
</Script>
)}
</>
)
}, [prerenderURL])
}
- ادمج المكوّنات مع التطبيق.
pages/_app.js
// Codelab: Add the SpeculationRules component.
import SpeculationRules from '../components/speculationrules'
function MyApp({ Component, pageProps }) {
useAnalyticsForSPA()
return (
<ResourceContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
{/* Codelab: Add SpeculationRules component */}
<SpeculationRules />
<Script id='analytics-for-mpa' strategy='beforeInteractive' src='/analytics.js' />
</ResourceContextProvider>
)
}
export default MyApp
- انقر على العرض المسبق.
يمكنك الآن ملاحظة التحسّن الكبير في سرعة التحميل. في حالة الاستخدام الفعلي، يتم بدء العرض المسبق للصفحة التي من المحتمل أن ينتقل إليها المستخدم بعد ذلك من خلال بعض الإرشادات.

إحصاءات Google
تلقائيًا، يرسل الملف analytics.js في تطبيق الويب النموذجي حدث مشاهدة صفحة عند وقوع الحدث DOMContentLoaded. للأسف، هذا ليس خيارًا جيدًا لأنّ هذا الحدث يتم تنشيطه أثناء مرحلة العرض المسبق.
لإضافة حدث document.prerendering وprerenderingchange لحلّ هذه المشكلة، اتّبِع الخطوات التالية:
- أعِد كتابة ملف
analytics.js:
public/analytics.js
const sendEvent = (type = 'pageview') => {
// Codelab: Make analytics prerendering compatible.
// The pageshow event could happen in the prerendered page before activation.
// The prerendered page should be handled by the prerenderingchange event.
if (document.prerendering) {
return
}
console.log(`Send ${type} event for MPA navigation.`)
fetch(`/api/analytics?from=${encodeURIComponent(location.pathname)}&type=${type}`)
}
...
// Codelab: Make analytics prerendering compatible.
// The prerenderingchange event is triggered when the page is activated.
document.addEventListener('prerenderingchange', () => {
console.log('The prerendered page was activated.')
sendEvent()
})
...
رائع، لقد عدّلت إحصاءاتك بنجاح لتصبح متوافقة مع العرض المسبق. يمكنك الآن الاطّلاع على سجلّات مشاهدة الصفحة بالتوقيت الصحيح في وحدة تحكّم المتصفّح.
4. إزالة عوائق bfcache
إزالة معالج الحدث unload
إنّ استخدام الحدث unload غير الضروري هو خطأ شائع جدًا لم يعُد يُنصح به. لا يمنع ذلك bfcache من العمل فحسب، بل إنّه غير موثوق به أيضًا. على سبيل المثال، لا يتم تفعيلها دائمًا على الأجهزة الجوّالة وSafari.
بدلاً من الحدث unload، يمكنك استخدام الحدث pagehide الذي يتم تنشيطه في جميع الحالات التي يتم فيها تنشيط الحدث unload وعندما يتم وضع صفحة في ذاكرة التخزين المؤقت للخلف والأمام.
لإزالة معالج الأحداث unload، اتّبِع الخطوات التالية:
- في الملف
analytics.js، استبدِل الرمز الخاص بمعالج الحدثunloadبالرمز الخاص بمعالج الحدثpagehide:
public/analytics.js
// Codelab: Remove the unload event handler for bfcache.
// The unload event handler prevents the content from being stored in bfcache. Use the pagehide event instead.
window.addEventListener('pagehide', () => {
sendEvent('leave')
})
تعديل عنوان cache-control
لا تستفيد الصفحات التي يتم عرضها مع عنوان HTTP Cache-control: no-store من ميزة "التخزين المؤقت للصفحات" في المتصفح، لذا من الممارسات الجيدة عدم الإفراط في استخدام هذا العنوان. على وجه الخصوص، إذا كانت الصفحة لا تحتوي على معلومات مخصَّصة أو مهمة، مثل حالة مُسجِّل الدخول، من المحتمل أنّك لست بحاجة إلى عرضها باستخدام عنوان HTTP Cache-control: no-store.
لتعديل عنوان cache-control في التطبيق النموذجي، اتّبِع الخطوات التالية:
- عدِّل الرمز
getServerSidePropsعلى النحو التالي:
pages/vegetables/index.js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
pages/vegetables/[name].js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res, query } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
تحديد ما إذا تمت استعادة صفحة من ذاكرة التخزين المؤقت للخلف والأمام
يتم تنشيط الحدث pageshow مباشرةً بعد الحدث load عند تحميل الصفحة في البداية وفي كل مرة تتم فيها استعادة الصفحة من التخزين المؤقت للصفحات. يحتوي الحدث pageshow على السمة persisted، وتكون قيمتها صحيحة إذا تمت استعادة الصفحة من ذاكرة التخزين المؤقت للخلف والأمام، وتكون قيمتها خاطئة إذا لم يتم ذلك. يمكنك استخدام السمة persisted للتمييز بين عمليات تحميل الصفحات العادية وعمليات استعادة الصفحات من ميزة "التخزين المؤقت للصفحات". من المفترض أنّ خدمات الإحصاءات الرئيسية على دراية بميزة bfcache، ولكن يمكنك التحقّق ممّا إذا تمت استعادة الصفحة من bfcache وإرسال الأحداث يدويًا.
لتحديد ما إذا تم استعادة صفحة من ذاكرة التخزين المؤقت للصفحات (bfcache)، اتّبِع الخطوات التالية:
- أضِف هذا الرمز إلى ملف
analytics.js.
public/analytics.js
// Codelab: Use the pageshow event handler for bfcache.
window.addEventListener('pageshow', (e) => {
// If the persisted flag exists, the page was restored from bfcache.
if (e.persisted) {
console.log('The page was restored from bfcache.')
sendEvent()
}
})
تصحيح أخطاء صفحة ويب
يمكن أن تساعدك "أدوات مطوّري برامج Chrome" في اختبار صفحاتك للتأكّد من أنّها محسّنة لاستخدام ذاكرة التخزين المؤقت للخلف والأمام، وتحديد أي مشاكل قد تجعلها غير مؤهَّلة.
لاختبار صفحة معيّنة، اتّبِع الخطوات التالية:
- انتقِل إلى الصفحة في Chrome.
- في "أدوات مطوّري برامج Chrome"، انقر على التطبيق > ذاكرة التخزين المؤقت للصفحات السابقة واللاحقة > تشغيل الاختبار.
تحاول "أدوات مطوّري برامج Chrome" الانتقال إلى صفحة أخرى ثم الرجوع إلى الصفحة الأصلية لتحديد ما إذا كان يمكن استعادة الصفحة من ميزة "التخزين المؤقت للصفحات".

في حال نجاح العملية، ستخبرك اللوحة بأنّه تم استعادة الصفحة من ميزة "التخزين المؤقت للصفحات":

في حال عدم نجاح عملية الاستعادة، ستُعلمك اللوحة بأنّه لم تتم استعادة الصفحة وستوضّح لك السبب. إذا كان السبب شيئًا يمكنك معالجته بصفتك مطوّرًا، ستخبرك اللوحة بذلك أيضًا.

5- تفعيل ميزة "الجلب المسبق من مواقع إلكترونية متعددة"
تبدأ عملية الجلب المُسبَق في جلب البيانات مبكرًا حتى تكون وحدات البايت متاحة في المتصفّح عندما يتنقّل المستخدم، ما يؤدي إلى تسريع عملية التنقّل. وهي طريقة سهلة لتحسين Core Web Vitals وتعويض بعض نشاط الشبكة قبل التنقّل. يؤدي ذلك إلى تسريع سرعة عرض أكبر محتوى مرئي (LCP) بشكل مباشر، ويتيح مساحة أكبر لمدة الاستجابة لأول إدخال (FID) ومتغيّرات التصميم التراكمية (CLS) عند التنقّل.
تتيح ميزة خادم وكيل للجلب المُسبَق الخاص الجلب المُسبَق من مواقع إلكترونية متعددة، ولكنّها لا تكشف عن معلومات خاصة بالمستخدم لخادم الوجهة.

تفعيل ميزة "الجلب المسبق لبيانات المواقع الإلكترونية" باستخدام Private Prefetch Proxy
يحتفظ مالكو المواقع الإلكترونية بإمكانية التحكّم في الجلب المسبق من خلال مصدر traffic-advice معروف، وهو مشابه لـ /robots.txt الخاص ببرامج الزحف على الويب، ما يتيح لخادم HTTP الإعلان عن أنّ على وكلاء التنفيذ تطبيق النصيحة ذات الصلة. في الوقت الحالي، يمكن لمالكي المواقع الإلكترونية أن ينصحوا وكيل المستخدم بعدم السماح بالاتصالات بالشبكة أو بتقييدها. وقد تتم إضافة نصائح أخرى في المستقبل.
لاستضافة مرجع حول نصائح بشأن حركة المرور، اتّبِع الخطوات التالية:
- أضِف الملف التالي المشابه لملف JSON:
public/.well-known/traffic-advice
[
{
"user_agent": "prefetch-proxy",
"google_prefetch_proxy_eap": {
"fraction": 1
}
}
]
الحقل google_prefetch_proxy_eap هو حقل خاص ببرنامج الاستخدام المبكر، والحقل fraction هو حقل للتحكّم في جزء عمليات الجلب المسبق المطلوبة التي يرسلها الخادم الوكيل الخاص بالجلب المسبق.
يجب عرض نصيحة حركة المرور بنوع MIME application/trafficadvice+json.
- في ملف
next.config.js، اضبط عنوان الاستجابة:
next.config.js
const nextConfig = {
// Codelab: Modify content-type for traffic advice file.
async headers() {
return [
{
source: '/.well-known/traffic-advice',
headers: [
{
key: 'Content-Type',
value: 'application/trafficadvice+json',
},
],
},
]
},
}
module.exports = nextConfig
6. دمج واجهة برمجة التطبيقات Shared Element Transitions API
عندما ينتقل المستخدم على الويب من صفحة إلى أخرى، يتغيّر المحتوى الذي يراه فجأة وبشكل غير متوقع مع اختفاء الصفحة الأولى وظهور الصفحة الجديدة. تؤدي تجربة المستخدم غير المتسلسلة وغير المتصلة هذه إلى إرباك المستخدم وتتطلّب مجهودًا ذهنيًا أكبر لأنّه يُضطر إلى تذكُّر كيفية وصوله إلى الصفحة الحالية. بالإضافة إلى ذلك، تزيد هذه التجربة من انطباع المستخدمين عن سرعة تحميل الصفحة أثناء انتظارهم تحميل الوجهة المطلوبة.
تساهم الرسوم المتحركة السلسة أثناء التحميل في تقليل الجهد الذهني لأنّ المستخدمين يبقون على دراية بالسياق أثناء التنقّل بين الصفحات، كما أنّها تقلّل من وقت الاستجابة المُدرَك أثناء التحميل لأنّ المستخدمين يشاهدون محتوًى جذّابًا وممتعًا في الوقت نفسه. لهذه الأسباب، توفّر معظم الأنظمة الأساسية عناصر أولية سهلة الاستخدام تتيح للمطوّرين إنشاء انتقالات سلسة، مثل Android وiOS وMacOS وWindows.
توفّر Shared Element Transitions API للمطوّرين الإمكانية نفسها على الويب، بغض النظر عمّا إذا كانت عمليات الانتقال بين المستندات (MPA) أو داخل المستند (SPA).

عروض توضيحية من pixiv و Tokopedia
لدمج واجهة برمجة التطبيقات Shared Element Transitions API في جزء التطبيق النموذجي الذي يتضمّن صفحة واحدة:
- أنشئ موضع إدراج مخصّصًا لإدارة الانتقال في الملف
use-page-transition.js:
utils/use-page-transition.js
import { useEffect, useContext, useRef, useCallback } from 'react'
import ResourceContext from '../components/resource-context'
// Call this hook on this first page before you start the page transition. For Shared Element Transitions, you need to call the transition.start() method before the next page begins to render, and you need to do the Document Object Model (DOM) modification or setting of new shared elements inside the callback so that this hook returns the promise and defers to the callback resolve.
export const usePageTransitionPrep = () => {
const { dispatch } = useContext(ResourceContext)
return (elm) => {
const sharedElements = elm.querySelectorAll('.shared-element')
// Feature detection
if (!document.createDocumentTransition) {
return null
}
return new Promise((resolve) => {
const transition = document.createDocumentTransition()
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
transition.start(async () => {
resolve()
await new Promise((resolver) => {
dispatch({ type: 'update', transition: { transition, resolver } })
})
})
})
}
}
// Call this hook on the second page. Inside the useEffect hook, you can refer to the actual DOM element and set them as shared elements with the transition.setElement() method. When the resolver function is called, the transition is initiated between the captured images and newly set shared elements.
export const usePageTransition = () => {
const { state, dispatch } = useContext(ResourceContext)
const ref = useRef(null)
const setRef = useCallback((node) => {
ref.current = node
}, [])
useEffect(() => {
if (!state.transition || !ref.current) {
return
}
const { transition, resolver } = state.transition
const sharedElements = ref.current.querySelectorAll('.shared-element')
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
resolver()
return () => {
dispatch({ type: 'update', transition: null })
}
})
return setRef
}
- استدعِ موضع الإدراج المخصّص
usePageTransitionPrep()في صفحة القائمة، ثم استدعِ الدالة غير المتزامنة لتفعيل الطريقةtransition.start()داخل الحدثclick.
داخل الدالة، يتم جمع عناصر الفئة shared-element وتسجيلها كعناصر مشتركة.
components/list-item.js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransitionPrep } from '../utils/use-page-transition'
...
function ListItemForSPA({ item, href }) {
// Codelab: Add Shared Element Transitions.
const transitionNextState = usePageTransitionPrep()
const handleClick = async (e) => {
const elm = e.target.closest('a')
await transitionNextState(elm)
}
return (
<Link href={href}>
<a className='block flex items-center' onClick={handleClick}>
<Icon src={item.image} name={item.name} className='shared-element' />
<div className='text-xl'>{item.name}</div>
</a>
</Link>
)
}
- في صفحة التفاصيل، استدعِ موضع الإدراج
usePageTransition()لإنهاء دالّة رد الاتصالtransition.start().
في دالة معاودة الاتصال هذه، يتم أيضًا تسجيل العناصر المشترَكة في صفحة التفاصيل.
pages/fruits/[name].js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransition } from '../../utils/use-page-transition'
const Item = ({ data }) => {
const { name, image, amountPer, nutrition } = data
// Codelab: Add the Shared Element Transitions API.
const ref = usePageTransition()
return (
<div className={'flex flex-col items-center justify-center py-4 px-4 sm:flex-row'} ref={ref}>
<div className='flex flex-col items-center sm:w-2/4'>
<Image
className='object-cover border-gray-100 border-2 rounded-full shared-element'
src={image}
width='240'
height='240'
alt={`picture of ${name}`}
/>
<h1 className='text-4xl font-bold mt-4'>{name}</h1>
</div>
<div className='sm:w-2/4 w-full'>
<Nutrition amountPer={amountPer} nutrition={nutrition} />
</div>
</div>
)
...
}
يمكنك الآن ملاحظة أنّ عناصر الصور تتم مشاركتها في صفحات القائمة والتفاصيل، ويتم ربطها بسلاسة عند الانتقال بين الصفحات. يمكنك حتى تخصيص الصورة المتحركة لجعلها أكثر جاذبية باستخدام عناصر CSS الزائفة.

7. تهانينا
تهانينا! لقد أنشأت تطبيقًا فوريًا وسلسًا على الويب يقدّم تجربة مستخدم جذابة وسهلة الاستخدام.
مزيد من المعلومات
العرض المُسبَق
- إعادة تصميم ميزة العرض المسبق
- تحميل الصفحات بشكل فوري في المتصفّح من خلال العرض المسبق التخميني
- quicklink
bfcache
الجلب المُسبَق على مواقع إلكترونية متعددة
Signed exchanges
عمليات الانتقال بين العناصر الجذرية/المشترَكة
- عمليات الانتقال بين العناصر المشتركة
- انتقالات سلسة وبسيطة بين الصفحات باستخدام Shared Element Transitions API
لا تزال واجهات برمجة التطبيقات هذه في المراحل الأولى من التطوير، لذا يُرجى مشاركة ملاحظاتك وآرائك على crbug.com أو كمشاكل في مستودع Github لواجهات برمجة التطبيقات ذات الصلة.