[Next] 六.next的優化

導出 html 並開啓服務

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

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

而後打開 package.json 並添加 scripts 爲如下內容:css

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

如今,您能夠 out 在項目內部的目錄中看到導出的 HTML 內容.html

如今須要在本地開啓一個靜態服務器,進行測試java

npm install -g serve

cd out

serve -p 8866

serve 是一個很是簡單的靜態 Web 服務器node

導出其餘頁面

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

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,併爲其添加新路徑並進行查詢.webpack

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

npm run export

cd out

serve -p 8080

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

添加 typescript

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

將 index.js 更改成 index.tsxweb

生成的 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

相關文章
相關標籤/搜索