Perché la mia app Shopify creata con Next.js (React) è così lenta da caricare?

Aug 15 2020

Ho seguito questo tutorial:https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react

Fin dall'inizio, la mia app è stata estremamente lenta da caricare, anche quando si cambia scheda, anche quando viene caricata tramite ngrok ed eseguita su localhost o distribuita su app engine.

Che cosa potrebbe causare questo ?

PS: sono nuovo nello sviluppo di app React, Next.js e Shopify, quindi la risposta potrebbe essere abbastanza semplice.

PPS: l'output della build sembra indicare che "First Load JS shared by all" è troppo grande in base al colore rosso. Non so come indagare su questo e ridurre le dimensioni di detti blocchi, anche se solo 214 KB non potrebbero spiegare tempi di caricamento così lenti, vero?


Costruire


Profiler degli strumenti di sviluppo di React


@next/bundle-analyzer Output:

Analizzato

Gzippato


pacchetto.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
      }
    }

Scheda Rete di Chrome Dev Tools

MODIFICARE:

npm run dev

Per qualche motivo, il tempo di caricamento della riga "webpack-hmr" continua ad aumentare costantemente.

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 (cliente)

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: "..."};
            }
    });
});

Modificare

Ho fornito una prima risposta, ma ne sto cercando una migliore se possibile.

Inoltre, sembra possibile fare in modo che i collegamenti di navigazione del bridge dell'app Shopify utilizzino il router next.js invece di attivare il ricaricamento della pagina intera:

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

Se qualcuno condivide come farlo per next.js con dettagli sufficienti, sarebbe meglio della mia risposta.

Risposte

Xandor Aug 15 2020 at 23:48

Il tuo caricamento iniziale sull'indice, secondo la cascata degli strumenti di sviluppo, ha richiesto quasi 2 secondi per soli 18,5 KB di dati. Questo è allarmantemente lento e prima che il resto delle tue risorse venga raggiunto anche. Il mio primo pensiero sarebbe stato il ritardo di rete/server. Lo stai ospitando localmente o su un server web di qualche tipo?

Lo ridurrei il più possibile, forse anche solo provare a caricare un semplice file index.html con solo un'intestazione. Se il caricamento richiede un paio di secondi, potrebbe essere necessario aggiornare o migrare a un host migliore. Se stai ospitando localmente, questo potrebbe essere solo un problema della tua Internet con una bassa velocità di caricamento. Molti piani Internet hanno download veloci ma caricamenti lenti e non sempre otterrai ciò che il tuo ISP promette.

LandryPlacid Aug 16 2020 at 20:11

prova a ottimizzare il tuo codice rimuovendo qualsiasi codice non necessario. Prova a utilizzare un'importazione più dinamica in modo da ottenere un caricamento iniziale veloce della piastra bioler e i codici pesanti come grafici, grafici, immagini e video vengono caricati in un secondo momento alla fine del client. import dynamic from "next/dynamic" , questo dovrebbe dare al cliente una rapida prima visualizzazione del disegno come fa YouTube.

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

prova a reagire Formik (un controllore di moduli ottimizzato per piccole app) e prova anche a utilizzare componenti di funzione su componenti di classe. Utilizzando Next puoi eseguire la maggior parte delle chiamate al database in getStatiProps, getServerSideProps, getStaticPaths. per il recupero periodico dalla cache utilizzare l'hook SWR.

TheProgrammer Aug 22 2020 at 21:37

Ho condiviso i tempi di caricamento misurati utilizzando npm run dev nella mia domanda, ma ecco alcune informazioni sui tempi di caricamento anche in modalità prod.

L'esecuzione dell'app in modalità prod ( npm run builde npm run start) dopo aver rimosso l'incorporamento nell'interfaccia utente di amministrazione di Shopify mostra che l'app impiega un totale di circa 2 secondi per caricarsi in modalità prod, il che sembra ancora molto lento (l'interfaccia utente di Shopify aggiungeva circa 3 secondi).

I collegamenti di navigazione della sposa dell'app Shopify ricaricano l'intera pagina quando si fa clic invece di modificare le pagine come farebbero i collegamenti Next.js.

Sostituiti i collegamenti di navigazione app con i collegamenti successivi.

Tuttavia, 1,86 secondi per il primo caricamento è molto lento e sono aperto a soluzioni migliori.