내 Shopify 앱이 Next.js (React)로 빌드 된 이유가 너무 느리나요?

Aug 15 2020

이 튜토리얼을 따랐습니다. https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react

처음부터 내 앱은 ngrok를 통해로드되고 localhost에서 실행되거나 앱 엔진에 배포 된 경우를 포함하여 탭을 변경할 때를 포함하여로드 속도가 매우 느 렸습니다.

원인은 무엇입니까?

추신 : 저는 React, Next.js 및 Shopify 앱 개발을 처음 사용하므로 대답은 매우 기본적 일 수 있습니다.

PPS : 빌드 출력에 "모두가 공유 한 첫 번째로드 JS"가 빨간색을 기준으로 너무 큽니다. 214KB만으로는 느린로드 시간을 설명 할 수 없지만 이것을 조사하고 해당 청크의 크기를 줄이는 방법을 모르겠습니다.


짓다


React Dev 도구 프로파일 러


@ next / bundle-analyzer 출력 :

파싱 ​​됨

GZipped


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 Dev Tools 네트워크 탭

편집하다:

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 초가 걸렸습니다. 이것은 놀라 울 정도로 느리고 나머지 리소스에 도달하기 전에 발생합니다. 내 첫 번째 생각은 네트워크 / 서버 지연입니다. 이것을 로컬로 호스팅하고 있습니까, 아니면 일종의 웹 서버에서 호스팅하고 있습니까?

헤더 만있는 간단한 index.html 파일을로드 할 수도 있습니다. 로드하는 데 몇 초가 걸리면 업그레이드하거나 더 나은 호스트로 마이그레이션해야 할 수 있습니다. 로컬에서 호스팅하는 경우 업로드 속도가 느린 인터넷 문제 일 수 있습니다. 많은 인터넷 계획은 다운로드가 빠르지 만 업로드 속도가 느리며 ISP가 약속 한 것을 항상 얻을 수있는 것은 아닙니다.

LandryPlacid Aug 16 2020 at 20:11

불필요한 코드를 제거하여 코드를 최적화하십시오. 보다 동적 인 가져 오기를 사용하여 빠른 초기 바이오 러 플레이트로드와 차트, 그래프, 사진 및 비디오로드와 같은 무거운 코드를 나중에 클라이언트 측에서로드하도록하십시오. import dynamic from "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 buildnpm run start) 에서 앱을 실행하면 여전히 매우 느린 것처럼 보이는 prod 모드에서 앱을로드하는 데 총 약 2 초가 소요됩니다 (Shopify UI가 약 3 초 추가됨).

Shopify 앱 신부 탐색 링크는 Next.js 링크와 같은 페이지를 변경하는 대신 클릭하면 전체 페이지를 다시로드합니다.

앱 탐색 링크를 다음 링크로 대체했습니다.

그럼에도 불구하고 첫 번째로드의 경우 1.86 초는 매우 느리고 더 나은 솔루션에 열려 있습니다.