前言
SSR指服務端渲染,即頁面是通過服務端渲染生成后返回給客戶端的,SSR主要為了提高頁面加載速度,改善用戶體驗,也可用於SEO搜索引擎優化。
Nuxt.js 官方定義: Nuxt.js 是一個基於 Vue 的通用應用框架。 通過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。我們的目標是創建一個靈活的應用框架,你可以基於它初始化新項目的基礎結構代碼,或者在已有 Node.js 項目中使用 Nuxt.js。
個人理解:Nuxt.js 就是預設了開發服務端渲染應用所需要的各種配置, 使用 Webpack 和 Node.js 進行封裝的基於Vue的SSR框架。
背景
我們部門從事的都是面對用戶的業務需求開發,面對用戶,意味着對頁面的體驗要求會更高,最直觀體驗是頁面首屏的加載速度,加載速度優化是我們體驗優化的長期、重要的一部分;本文的起源正是首屏加載速度優化。
頁面加載速度優化的核心包括三點:減少資源文件的請求數量;減小每個資源文件的大小;提高每個資源的加載速度;
諸如合並API訪問,壓縮混淆文件,支持webp圖片,資源cdn緩存等等常用辦法,都是以上面三個核心為出發點的; 這些常用辦法基本都可以通過webpack配置,公司基礎服務,代碼較小的變更完成。
我們負責的各主流量入口頁面,已基本做過以上常用的優化,但由於主入口頁面資源量較大的原因,優化后並不能達到預期的效果,我們需要探索其它優化方案。 我們快速想到了用SSR的方案進一步解決加載速度問題,從零開始的搭建服務端渲染應用相當復雜,肯定會涉及到服務端的開發,作為獨立的前端團隊,成本較高昂; 我們決定嘗試是否能找到一種成本較低的現有SSR框架,以達到目的;
因主入口頁面技術棧為vue,方案調研中自然而然的看到了Nuxt.js此種基於Vue的SSR框架; Nuxt.js和項目技術棧匹配度急高,學習成本極低,自然成為我們的第一選擇;
我們引入Nuxt.js,最初只是利用了服務端異步獲取API接口數據和服務端渲染兩項功能,去重構了我們的項目,重構后效果基本達到我們的預期,正常網絡狀態下,基本可以達到秒開; 入口頁面,團隊是作為一個長期的項目進行不定期優化的,我們逐步圍繞Nuxt.js框架,對項目做了進一步優化升級,本文主要介紹我們Nuxt.js頁面優化的進一步探索與實踐
探索與實踐
我們主要的探索與實踐可行方向主要有兩個:
一、Nuxt.js特性合理應用
應用到的特性主要包括asyncData異步獲取數據、mounted不支持服務端渲染、no-ssr組件不在服務端渲染中呈現;
通過相關特性做到API數據和頁面結構合理拆分,首屏所需數據和結構通過服務端獲取並渲染,非首屏數據和結構通過客戶端獲取並渲染。
示例代碼:
no-ssr結構拆分
<template> <div> <!-- 頂部banner --> <banner :banner="banner" /> <!-- 非首屏所需結構,通過no-ssr組件達到不在服務端渲染目的--> <no-ssr> <!-- 商品列表 --> <prod-list :listData="listData"/> </no-ssr> </div> </template
API數據拆分
export default { async asyncData({ app, query }) { try { // 獲取頁面頂部輪播圖信息 const getBanner = () => { return app.$axios.$get('zz/zy/banner') } // 獲取底部配置信息 const getFooter = () => { return app.$axios.$get('zz/zy/footer', { params: { smark: query.smark } }) } // 並發獲取首屏數據,服務端獲取 const [banner, footer] = await Promise.all([getBanner(), getFooter()]) return {banner: banner, footer: footer} } catch (e) { console.log('interface timeout or format error => ', e) return {} } }, mounted() { // 非首屏使用的數據, 客戶端獲取 this.loadListData() }, methods: { loadListData() { this.$axios.$get('zz/zy/list').then(() => { // 數據處理邏輯 }) } } }
二、服務端引入緩存
服務端開發意味着緩存可作為性能優化的最直接法門,Nuxt.js作為一種服務端渲染框架,也不例外;針對不同的頁面,不同的數據狀態,可主要區分為下面三類緩存:
1、API接口數據緩存
將服務端獲取的數據,全部緩存到node進程內存中,定時刷新,有效期內請求都通過緩存獲取API接口數據,減小數據獲取時間;
此種緩存適用於緩存的部分API數據,基本保持不變,變更不頻繁,與用戶個人數據無關。
示例代碼
import LRU from 'lru-cache' const CACHED = new LRU({ max: 100, // 緩存隊列長度 maxAge: 1000 * 60 // 緩存時間 }) export default { async asyncData({ app, query }) { try { let banner, footer if (CACHED.has('baseData')) { // 存在緩存,使用緩存數據 let data = CACHED.get('baseData') data = JSON.parse(data) banner = data.banner footer = data.footer } else { // 獲取頁面頂部輪播圖信息 const getBanner = () => { return app.$axios.$get('zz/zy/banner') } // 獲取底部配置信息 const getFooter = () => { return app.$axios.$get('zz/zy/footer', { params: { smark: query.smark } }) } [banner, footer] = await Promise.all([getBanner(), getFooter()]) // 將數據寫入緩存 CACHED.set('baseData', JSON.stringify({ banner: banner, footer: footer})) } return {mods: mods, footer: footer} } catch (e) { console.log('interface timeout or format error => ', e) return {} } } }
2、組件級別緩存
將渲染后的組件DOM結構存入緩存,定時刷新,有效期通過緩存獲取組件DOM結構,減小生成DOM結構所需時間;
適用於渲染后結構不變或只有幾種變換、並不影響上下文的組件。
示例代碼:
nuxt.config.js配置項修改
const LRU = require('lru-cache') module.exports = { render: { bundleRenderer: { cache: LRU({ max: 1000, // 緩存隊列長度 maxAge: 1000 * 60 // 緩存1分鍾 }) } } }
需要做緩存的 vue 組件, 需增加 name 以及 serverCacheKey 字段,以確定緩存的唯一鍵值。
export default { name: 'zzZyHome', props: ['type'], serverCacheKey: props => props.type }
如果組件依賴於很多的全局狀態,或者狀態取值非常多,緩存會因頻繁被設置而導致溢出,這樣的組件做緩存就沒有多大意義了;
另外組件緩存,只是緩存了dom結構,如created等鈎子中的代碼邏輯並不會被緩存,如果其中邏輯會影響上下邊變更,是不會再執行的,此種組件也不適合緩存。
3、頁面整體緩存
當整個頁面與用戶數據無關,依賴的數據基本不變的情況下,可以對整個頁面做緩存,減小頁面獲取時間;
頁面整體緩存前提是在使用Nuxt.js腳手架工具create-nuxt-app初始化項目時,必須選擇集成服務器框架,如express、koa,只有這樣才具有服務端中間件擴展的功能。
示例代碼:
服務端中間件middleware/page-cache.js
const LRU = require('lru-cache') let cachePage = new LRU({ max: 100, // 緩存隊列長度 maxAge: 1000 * 60 // 緩存1分鍾 }) export default function(req, res, next){ let url = req._parsedOriginalUrl let pathname = url.pathname // 通過路由判斷,只有首頁才進行緩存 if (['/home'].indexOf(pathname) > -1) { const existsHtml = cachePage.get('homeData') if (existsHtml) { return res.end(existsHtml.html, 'utf-8') } else { res.original_end = res.end // 重寫res.end res.end = function (data) { if (res.statusCode === 200) { // 設置緩存 cachePage.set('homeData', { html: data}) } // 最終返回結果 res.original_end(data, 'utf-8') } } } next() }
nuxt.config.js配置項修改,引入服務端中間件
//針對home路由做緩存 serverMiddleware: [ { path: '/home', handler: '~/middleware/page-cache.js' }, ]