Next.js(React)でビルドされたShopifyアプリの読み込みが遅いのはなぜですか?

Aug 15 2020

私はこのチュートリアルに従いました: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react

最初から、ngrokを介してロードしてローカルホストで実行したり、アプリエンジンにデプロイしたりする場合など、タブを変更する場合を含め、アプリのロードが非常に遅くなりました。

これを引き起こしている可能性があるのは何ですか?

PS:私はReact、Next.js、Shopifyアプリの開発に不慣れなので、答えは非常に基本的なものになる可能性があります。

PPS:ビルド出力は、赤い色に基づいて「すべてが共有するFirst LoadJS」が大きすぎることを示しているようです。たった214KBではこのような遅いロード時間を説明できませんでしたが、これを調査してチャンクのサイズを縮小する方法がわかりません。


ビルド


React Dev Tools Profiler


@ next / bundle-analyzer出力:

解析済み

Gzip圧縮


package.json

 {
      "name": "ShopifyApp1",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev": "node server.js NODE_ENV=dev",
        "build": "next build",
        "deploy": "next build && gcloud app deploy --version=deploy",
        "start": "NODE_ENV=production node server.js",
        "analyze": "cross-env ANALYZE=true npm run build"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@google-cloud/storage": "^5.2.0",
        "@next/bundle-analyzer": "^9.5.2",
        "@sendgrid/mail": "^7.2.3",
        "@shopify/app-bridge-react": "^1.26.2",
        "@shopify/koa-shopify-auth": "^3.1.65",
        "@shopify/koa-shopify-graphql-proxy": "^4.0.1",
        "@shopify/koa-shopify-webhooks": "^2.4.3",
        "@shopify/polaris": "^5.1.0",
        "@zeit/next-css": "^1.0.1",
        "apollo-boost": "^0.4.9",
        "cors": "^2.8.5",
        "cross-env": "^7.0.2",
        "dotenv": "^8.2.0",
        "email-validator": "^2.0.4",
        "extract-domain": "^2.2.1",
        "firebase-admin": "^9.0.0",
        "graphql": "^15.3.0",
        "helmet": "^4.0.0",
        "isomorphic-fetch": "^2.2.1",
        "js-cookie": "^2.2.1",
        "koa": "^2.13.0",
        "koa-body": "^4.2.0",
        "koa-bodyparser": "^4.3.0",
        "koa-helmet": "^5.2.0",
        "koa-router": "^9.1.0",
        "koa-session": "^6.0.0",
        "next": "^9.5.1",
        "react": "^16.13.1",
        "react-apollo": "^3.1.5",
        "react-dom": "^16.13.1",
        "react-infinite-scroll-component": "^5.0.5",
        "sanitize-html": "^1.27.2",
        "scheduler": "^0.19.1",
        "store-js": "^2.0.4",
        "tldts": "^5.6.46"
      },
      "devDependencies": {
        "webpack-bundle-analyzer": "^3.8.0",
        "webpack-bundle-size-analyzer": "^3.1.0"
      },
      "browser": {
        "@google-cloud/storage": false,
        "@sendgrid/mail": false,
        "@shopify/koa-shopify-auth": false,
        "@shopify/koa-shopify-graphql-proxy": false,
        "@shopify/koa-shopify-webhooks": false,
        "cors": false,
        "email-validator": false,
        "extract-domain": false,
        "firebase-admin": false,
        "graphql": false,
        "helmet": false,
        "isomorphic-fetch": false,
        "koa": false,
        "koa-body": false,
        "koa-bodyparser": false,
        "koa-helmet": false,
        "koa-router": false,
        "koa-session": false,
        "sanitize-html": false,
        "tldts": false
      }
    }

Chrome開発ツールの[ネットワーク]タブ

編集:

npm run dev

何らかの理由で、「webpack-hmr」ラインのロード時間は絶えず増加し続けています。

npm run build && npm run start


next.config.js

require("dotenv").config({path:"live.env"});
const withCSS = require('@zeit/next-css');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const withBundleAnalyzer = require('@next/bundle-analyzer')({enabled: process.env.ANALYZE === 'true'})
const apiKey =  JSON.stringify(process.env.SHOPIFY_API_KEY);
module.exports =  withBundleAnalyzer(
  withCSS({
    distDir: 'build',
    webpack: (config) => {
      const env = { API_KEY: apiKey };
      config.plugins.push(new webpack.DefinePlugin(env));
      config.plugins.push(new webpack.DefinePlugin(new BundleAnalyzerPlugin()));
      config.resolve = {
        alias: {
          'react-dom$': 'react-dom/profiling',
          'scheduler/tracing': 'scheduler/tracing-profiling'
        },
        ...config.resolve
      };
      return config;
    }
  })
);

_app.js

import App from 'next/app';
import Head from 'next/head';
import { AppProvider } from '@shopify/polaris';
import { Provider } from '@shopify/app-bridge-react';
import '@shopify/polaris/dist/styles.css'
import translations from '@shopify/polaris/locales/en.json';
import Cookies from 'js-cookie';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';

const client = new ApolloClient({
  fetchOptions: {
    credentials: 'include'
  },
});

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props;
    const config = { apiKey: API_KEY, shopOrigin: Cookies.get("shopOrigin"), forceRedirect: true };
    return (
      <React.Fragment>
        <Head>
          <title>...</title>
          <meta charSet="utf-8" />
        </Head>
        <Provider config={config}>
          <AppProvider i18n={translations}>
            <ApolloProvider client={client}>
              <Component {...pageProps} />
            </ApolloProvider>
          </AppProvider>
        </Provider>
      </React.Fragment>
    );
  }
}

export default MyApp;

Index.js(クライアント)

import {
  Button,
  Card,
  Form,
  FormLayout,
  Layout,
  Page,
  Stack,
  TextField,
  DisplayText,
  Toast,
  Frame
} from '@shopify/polaris';

class Index extends React.Component  {

  state = {
    emails: '',
    domain: '' ,
    alias: '',
    err: '',
    message: '',
    active: false,
    loadingDomainResponse: false,
    loadingEmailResponse: false
  };

  componentDidMount() {
    fetch(`/state`, {
      method: 'GET'
    }).then(response => response.json())
    .then(result => {
      if (result.err) {
        this.setState({
          err: result.err,
          message: result.err,
          active: true
        })
      }
      else {
        this.setState({
          emails: result.emails,
          domain: result.domain,
          alias: result.alias
        })
      }
    });
  };

  

  render() {

    const { emails, domain, alias, err, message, active, loadingEmailResponse, loadingDomainResponse} = this.state;
    

    const toastMarkup = active ? (
      <Toast content={message} error={err} onDismiss={this.handleToast}/> 
    ) : null;


    return (
      <Frame>
         <Page>
          {toastMarkup}
          <Layout>
            <Layout.AnnotatedSection
              title="..."
              description="..."
            >
              <Card sectioned>
                <Form onSubmit={this.handleSubmitEmails}>
                  <FormLayout>
                    <TextField
                      value={emails}
                      onChange={this.handleChange('emails')}
                      label="..."
                      type="emails"
                      maxlength="200"
                    />
                    <Stack distribution="trailing">
                      <Button primary submit loading={loadingEmailResponse}>
                        Save
                      </Button>
                    </Stack>
                  </FormLayout>
                </Form>
              </Card>
            </Layout.AnnotatedSection>
            <Layout.AnnotatedSection
              title="..."
              description="..."
            >
              <Card sectioned>
                <DisplayText size="small"> {domain} </DisplayText>
                <br/>
                <Form onSubmit={this.handleSubmitDomain}>
                  <FormLayout>
                    <TextField
                      value={alias}
                      onChange={this.handleChange('alias')}
                      label="..."
                      type="text"
                      maxlength="50"
                    />
                    <Stack distribution="trailing">
                      <Button primary submit loading={loadingDomainResponse}>
                        Save
                      </Button>
                    </Stack>
                  </FormLayout>
                </Form>
              </Card>
            </Layout.AnnotatedSection>
          </Layout> 
       </Page>
      </Frame>
    );
  }

  handleToast = () => {
    this.setState({
      err: false,
      message: false,
      active: false
    })
  };
  
  handleSubmitEmails = () => {
    this.setState({loadingEmailResponse:true});
    fetch(`/emails`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        emails: this.state.emails
      })
    }).then(response => response.json())
    .then(result => {
      console.log("JSON: "+JSON.stringify(result));
      if (result.err) {
        this.setState({
          err: result.err,
          message: result.err,
          active: true
        })
      }
      else {
        this.setState({message: "...", active: true});
      }
      this.setState({loadingEmailResponse:false});
    });
  };

  handleSubmitDomain = () => {
    this.setState({loadingDomainResponse:true});
    fetch(`/domain`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: 
        JSON.stringify({
          alias: this.state.alias
        })
      }).then(response => response.json())
      .then(result => {
        console.log("JSON: "+JSON.stringify(result));
        if (result.err) {
          this.setState({
            err: result.err,
            message: result.err,
            active: true
          })
        }
        else {
          this.setState({message: "...", active: true});
        }
        this.setState({loadingDomainResponse:false});
      });
  };

  


  handleChange = (field) => {
    return (value) => this.setState({ [field]: value });
  };
}

export default Index;

server.js

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
app.prepare().then(() => {
    const server = new Koa();
    const router = new Router();
    server.use(bodyParser());
    server.use(session({ secure: true, sameSite: 'none' }, server));
    server.keys = [SHOPIFY_API_SECRET_KEY];
    router.get('/state',  async (ctx) => {
        let domain = ctx.session.shop;
        let alias;
        const snap = await global.db.collection("...").doc(ctx.session.shop).get();
            if (snap.data().alias) {
                alias = snap.data().alias;
            }
            let emails = snap.data().emails;
            let emailString = "";
            if (!emails) {
                ctx.response.body = {err: "..."};
            }
            else if(emails.length < 4) {
                for (email of emails) {
                    emailString += (","+email);
                }
                theEmailString = emailString.substring(1);
                let response = {
                    domain: domain,
                    alias: alias,
                    emails: theEmailString
                }
                ctx.response.body = response;
            } 
            else {
                ctx.response.body = {err: "..."};
            }
    });
});

編集

私は最初の答えを提供しましたが、可能であればより良いものを探しています。

また、Shopifyアプリのブリッジナビゲーションリンクで、ページ全体のリロードをトリガーする代わりに、next.jsルーターを使用することも可能です。

https://shopify.dev/tools/app-bridge/actions/navigation

誰かがnext.jsでそれを行う方法を十分に詳細に共有している場合、それは私の答えよりも優れています。

回答

Xandor Aug 15 2020 at 23:48

開発ツールのウォーターフォールによると、インデックスへの最初のロードは、わずか18.5KBのデータでほぼ2秒かかりました。これは驚くほど遅く、残りのリソースに到達する前ですらあります。私が最初に考えたのは、ネットワーク/サーバーのラグです。これをローカルでホストしていますか、それとも何らかのWebサーバーでホストしていますか?

ヘッダーのみを含む単純なindex.htmlファイルをロードしてみることもできますが、できる限り削除します。ロードに数秒かかる場合は、アップグレードするか、より適切なホストに移行する必要があります。ローカルでホストしている場合、これはアップロード速度が遅いインターネットの問題である可能性があります。多くのインターネットプランでは、ダウンロードは高速ですがアップロードは低速であり、ISPが約束したものを常に得られるとは限りません。

LandryPlacid Aug 16 2020 at 20:11

不要なコードを削除して、コードを最適化してみてください。より動的なインポートを使用して、最初のバイオラープレートのロードを高速化し、後でクライアント側でチャート、グラフ、画像、ビデオなどの重いコードをロードするようにしてください。「next / dynamic」からダイナミックをインポートします。これにより、クライアントはyoutubeのように最初のペイントビューをすばやく表示できます。

https://nextjs.org/docs/advanced-features/dynamic-import

react Formik(小さなアプリ用の最適化フォームコントローラー)を試してみてください。また、クラスコンポーネントよりも関数コンポーネントを使用してみてください。Nextを使用すると、ほとんどのデータベース呼び出しをgetStatiProps、getServerSideProps、getStaticPathsで実行できます。定期的にキャッシュされるフェッチには、SWRフックを使用します。

TheProgrammer Aug 22 2020 at 21:37

私の質問では、npm run devを使用して測定されたロード時間を共有しましたが、prodモードでのロード時間に関する情報もここにあります。

Shopify管理UIへの埋め込みを削除した後にアプリをprodモード(npm run buildおよびnpm run start)で実行すると、アプリがprodモードで読み込まれるまでに合計約2秒かかることがわかりますが、それでも非常に遅いようです(Shopify UIは約3秒追加されていました)。

Shopifyアプリの花嫁ナビゲーションリンクは、Next.jsリンクのようにページを変更する代わりに、クリックするとページ全体をリロードします。

アプリのナビゲーションリンクを次のリンクに置き換えました。

それでも、最初のロードの1.86秒は非常に遅く、より良いソリューションを受け入れることができます。