[Next] 六.next的優化


導出 html 並開啟服務

我們將 pages 下頁面導出為靜態 HTML 頁面.首先,next.config.js 在應用程序的根目錄中創建一個名為的文件,並添加以下內容

exportPathMap: async function() {
    return {
        "/": { page: "/" },
        "/books": { page: "/books" },
        "/article": { page: "/article" },
        "/write": { page: "/write" }
    };
},

然后打開 package.json 並添加 scripts 為以下內容:

  "build": "next build",
  "export": "next export"

現在,您可以 out 在項目內部的目錄中看到導出的 HTML 內容.

現在需要在本地開啟一個靜態服務器,進行測試

npm install -g serve

cd out

serve -p 8866

serve 是一個非常簡單的靜態 Web 服務器

導出其他頁面

將以下內容添加到 next.config.js 文件中:

  exportPathMap: async function() {
    const paths = {
      "/": { page: "/" },
      "/books": { page: "/books" },
      "/article": { page: "/article" },
      "/write": { page: "/write" }
    };

    const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
    const data = await res.json();
    const shows = data.map(entry => entry.show);

    shows.forEach(show => {
      paths[`/book/${show.id}`] = {
        page: "/book/[id]",
        query: { id: show.id }
      };
    });

    return paths;
  },

為了渲染詳情頁面,我們首先獲取數據列表.然后,我們循環獲取 id,並為其添加新路徑並進行查詢.

關閉本地服務器並在次執行

npm run export

cd out

serve -p 8080

運行 next export 命令時,Next.js 不會構建應用程序.頁面/book/[id]已經存在於構建中,因此無需再次構建整個應用程序.但是,如果我們對應用程序進行了任何更改,則需要再次構建應用程序以獲取這些更改,就是在執行一個 npm run build

添加 typescript

npm install --save-dev typescript @types/react @types/node @types/react-dom

將 index.js 更改為 index.tsx

生成的 tsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": ["node_modules"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

index.tsx 頁面提示缺少類型,因為我們沒有告訴 TypeScript 它是 Next.js 頁面,在 strict 模式下不允許隱式 any 類型.

import { NextPage } from 'next';

const Home: NextPage<{ userAgent: string }> = ({ userAgent }) => (
  <h1>Hello world! - user agent: {userAgent}</h1>
);

Home.getInitialProps = async ({ req }) => {
  const userAgent = req ? req.headers['user-agent'] || '' : navigator.userAgent;
  return { userAgent };
};

export default Home;

懶加載模塊

創建 firebase 頁面

整體項目代碼 官方案例

添加 analyzer

安裝依賴包

npm install firebase @zeit/next-bundle-analyzer cross-env --save

然后打開 package.json 並添加 scripts 為以下內容:

"analyze": "cross-env ANALYZE=true next build",
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build

現在的 next.config.js 所有配置

const fetch = require("isomorphic-unfetch");
const withBundleAnalyzer = require("@zeit/next-bundle-analyzer");
const withLess = require("@zeit/next-less");
const FilterWarningsPlugin = require("webpack-filter-warnings-plugin");

if (typeof require !== "undefined") {
  require.extensions[".less"] = file => {};
}

function HACK_removeMinimizeOptionFromCssLoaders(config) {
  config.module.rules.forEach(rule => {
    if (Array.isArray(rule.use)) {
      rule.use.forEach(u => {
        if (u.loader === "css-loader" && u.options) {
          delete u.options.minimize;
        }
      });
    }
  });
}

module.exports = withBundleAnalyzer(
  withLess({
    analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
    analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
    bundleAnalyzerConfig: {
      server: {
        analyzerMode: "static",
        reportFilename: "../bundles/server.html"
      },
      browser: {
        analyzerMode: "static",
        reportFilename: "../bundles/client.html"
      }
    },
    exportPathMap: async function() {
      const paths = {
        "/": { page: "/" },
        "/books": { page: "/books" },
        "/article": { page: "/article" },
        "/write": { page: "/write" }
      };

      const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
      const data = await res.json();
      const shows = data.map(entry => entry.show);

      shows.forEach(show => {
        paths[`/book/${show.id}`] = {
          page: "/book/[id]",
          query: { id: show.id }
        };
      });

      return paths;
    },
    lessLoaderOptions: {
      javascriptEnabled: true
    },
    webpack(config) {
      config.plugins.push(
        new FilterWarningsPlugin({
          exclude: /mini-css-extract-plugin[^]*Conflicting order between:/
        })
      );
      HACK_removeMinimizeOptionFromCssLoaders(config);
      return config;
    }
  })
);

直接執行

npm run analyze

服務器文件分析

客戶端文件分析

firebase 文件分析詳情

可以看到當前 firebase 和 firebase/[id].js 存在對 firebase 模塊的引用

延遲加載

僅當用戶嘗試導航到其他頁面時,我們才使用 firebase 模塊.可以使用 Next.js 的動態導入功能輕松地做到這一點.

修改 lib/load-db.js

export default async function loadDb() {
  const firebase = await import('firebase/app');
  await import('firebase/database');

  try {
    firebase.initializeApp({
      databaseURL: 'https://hacker-news.firebaseio.com'
    });
  } catch (err) {
    // we skip the "already exists" message which is
    // not an actual error when we're hot-reloading
    if (!/already exists/.test(err.message)) {
      console.error('Firebase initialization error', err.stack);
    }
  }

  return firebase.database().ref('v0');
}

使用 import()函數加載 firebase 模塊,用 await 來等待並解析模塊.

再次執行

npm run analyze

firebase 模塊具有自己的 bundle,static/chunks/[a-random-string].js.當您嘗試導入 firebase/app 和 firebase/database 模塊時,將加載此 bundle.


可以看到,firebse 和 firebase/[id].js 文件縮小了不少

進行測試

由於需要更真實的測試在線上的表現,我們需要重新構建.

npm run build
npm run start

然后輸入 localhost:8866 (與 dev 不一樣),之后進入 firebase 頁面在進入 firebase 詳情頁面.

實際上只會第一次瀏覽頁面時加載,當 firebase 頁面導入 firebase/app 和 firebase/database 模塊,會加載 firebase 的 bundle.等再次進入的時候,改 bundle 已經加載過,就不會再次加載`

如圖,再次加載沒有請求

延遲加載的模塊減少了主要 JavaScript 包的大小,帶來了更快的加載速度

使用 import 的要點

  async componentDidMount() {
    const SimpleMDE = await import("simplemde");
    const marked = await import("marked");
    const hljs = await import("highlight.js");
  ......

  new SimpleMDE.default()
  hljs.default.highlightAuto(code).value
  marked.default
  }

與正常的 import 加載不用的是,import('xxx')加載的形式會將返回的模塊放到一個 default 字段中進行保存

延遲加載組件

在一個組件里面同時使用 3 個 markdown 相關組件

import Markdown from "react-markdown";
import marked from "marked";
import Highlight from "react-highlight";

導致這個頁面過於龐大

執行npm run analyze看看 markdown/[id]大小

但是我們不需要在一開始就使用這些模塊,只有需要加載 markdown 文本時才需要.因此,如果我們僅在使用時才加載,那將大大減少初始 bundle,有助於頁面快地加載.

使用 HOC 高階組件抽離渲染

新建 lib/with-post.js

import Layout from "../components/MyLayout";
import dynamic from "next/dynamic";
import marked from "marked";

const Highlight = dynamic(() => import("react-highlight"));

marked &&
  marked.setOptions({
    gfm: true,
    tables: true,
    breaks: true
  });

function WithPost(InnerComponent, options) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.renderMarkdown = this.renderMarkdown.bind(this);
    }

    renderMarkdown(id) {
      // If a code snippet contains in the markdown content
      // then use Highlight component
      if (id === 1 || id === "1") {
        return (
          <Layout>
            <h1>{options.title}</h1>
            <h3>當前id=>{id}</h3>
            <div className="markdown">
              <Highlight innerHTML>{marked(options.content)}</Highlight>
            </div>
          </Layout>
        );
      }

      // If not, simply render the generated HTML from markdown
      return (
        <Layout>
          <h1>{options.title}</h1>
          <h3>當前id=>{id}</h3>
          <div className="markdown">
            <div dangerouslySetInnerHTML={{ __html: marked(options.content) }} />
          </div>
        </Layout>
      );
    }

    render() {
      return <InnerComponent renderMarkdown={this.renderMarkdown}></InnerComponent>;
    }
  };
}

export default WithPost;

修改 marked/[id].js

import React, { Component } from "react";
import withPost from "../../lib/with-post";
import { withRouter } from "next/router";

const data = {
  title: "Deploy apps with ZEIT now",
  content: `
          Deploying apps to ZEIT now is pretty easy.
          Simply run the following command from your app root:
          ~~~bash
          npm i -g now # one time command
          now
          ~~~
        `
};

class Post extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <div>{this.props.renderMarkdown(this.props.router.query.id)}</div>;
  }
}

Post = withRouter(Post);
Post = withPost(Post, data);
export default Post;

現在需要使用 Next.js 中的動態導入將 react-highlight 組件轉換為動態組件.最終實現這些組件僅在將要在頁面中呈現時才加載.可以使用該 next/dynamic 來創建動態組件.

動態組件

//import Highlight from 'react-highlight'
import dynamic from 'next/dynamic';

const Highlight = dynamic(() => import('react-highlight'));

訪問 localhost:6688,可以在 network 中找到單次 Highlight 的 bundle 引入

僅在需要時加載

if (id === 1 || id === "1") {
  return (
    <Layout>
      <h1>{options.title}</h1>
      <h3>當前id=>{id}</h3>
      <div className="markdown">
        <Highlight innerHTML>{marked(options.content)}</Highlight>
      </div>
    </Layout>
  );
}

當前判斷 id 是否為 1,如果是就加載 Highlight,否則就正常插入 html

使用動態組件后,就會將組件單獨實現一個 bundle,加載時候直接加載這一個 bundle 就行了

效果也是實現了 javascript 主文件的精簡,同時所以 marked/[id].js 的大小,能夠根據實際來判斷是否加載一大段可能不需要的代碼.

為了模擬真實的服務器渲染效果,需要重新構建

npm run build
npm run start

上圖中可以看到,highlight 的 bundle 名稱是 16.[chunkname].js

輸入http://localhost:8866/marked/1,可以在head里面發現<link rel="preload" href="/_next/static/chunks/commons.972eca8099a2576b25d9.js" as="script">的存在.之后再切換為其它的 id,這一個 js 文件就沒有在 head 中引入

創建 awp 頁面

新建 pages/awp.js

export const config = { amp: true };

export default function Awp(props) {
  return <p>Welcome to the AMP only Index page!!</p>;
}

AMP,來自 Google 的移動頁面優化方案

通過添加 amp: 'hybrid'以下內容來創建混合 AMP 頁面

import { useAmp } from 'next/amp';

export const config = { amp: 'hybrid' };

export default function Awp(props) {
  const isAmp = useAmp();
  return <p>Welcome to the {isAmp ? 'AMP' : 'normal'} version of the Index page!!</p>;
}

自動靜態優化

自動靜態優化

如果沒有阻塞數據要求,則 Next.js 會自動確定頁面為靜態頁面(可以預呈現).判斷標准就是 getInitialProps 在頁面中是否存在.

如果 getInitialProps 存在,則 Next.js 不會靜態優化頁面.相反,Next.js 將使用其默認行為並按請求呈現頁面(即服務器端呈現).

如果 getInitialProps 不存在,則 Next.js 會通過將其預呈現為靜態 HTML 來自動靜態優化您的頁面.在預渲染期間,路由器的 query 對象將為空,因為 query 在此階段我們沒有信息要提供.query 水合后,將在客戶端填充任何值.

此功能允許 Next.js 發出包含服務器渲染頁面和靜態生成頁面的混合應用程序.這樣可以確保 Next.js 始終默認發出快速的應用程序.

靜態生成的頁面仍然是反應性的:Next.js 將對您的應用程序客戶端進行水化處理,使其具有完全的交互性.

優點是優化的頁面不需要服務器端計算,並且可以立即從 CDN 位置流式傳輸到最終用戶.為用戶帶來超快的加載體驗.

  • 在大多數情況下,你並不需要一個自定義的服務器,所以嘗試添加 target: 'serverless'
  • getInitialProps 是頁面是否靜態的主要決定因素,如果不需要 SSR,請不要添加到頁面
  • 並非所有動態數據都必須具有 SSR,例如,如果它在登錄后,或者您不需要 SEO,那么在這種情況下,最好在外部進行獲取 getInitialProps 使用靜態 HTML 加載速度

Doc


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM