Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
147 views
in Technique[技术] by (71.8m points)

node.js - How could I reduce the long cold start time when doing SSR from Lambda

I have an SSR server for React.JS app made with CRA, ConnectedRouter, Redux and Saga. I am trying to host this server under AWS Lambda using the below code:

const serverlessExpress = require('@vendia/serverless-express');
const app = require('./index');
const binaryMimeTypes = [
  'application/javascript',
  ...
  'text/xml'
];
exports.handler = serverlessExpress({
  app,
  binaryMimeTypes
}).handler;

This wrapper pulls the setup code below:

const md5File = require('md5-file');
const fs = require('fs');
const path = require('path');

// CSS styles will be imported on load and that complicates matters... ignore those bad boys!
const ignoreStyles = require('ignore-styles');
const register = ignoreStyles.default;

// We also want to ignore all image requests
// When running locally these will load from a standard import
// When running on the server, we want to load via their hashed version in the build folder
const extensions = ['.gif', '.jpeg', '.jpg', '.png', '.svg'];

// Override the default style ignorer, also modifying all image requests
register(ignoreStyles.DEFAULT_EXTENSIONS, (mod, filename) => {
  if (!extensions.find(f => filename.endsWith(f))) {
    // If we find a style
    return ignoreStyles.noOp();
  }
  // for images that less than 10k, CRA will turn it into Base64 string, but here we have to do it again
  const stats = fs.statSync(filename);
  const fileSizeInBytes = stats.size / 1024;
  if (fileSizeInBytes <= 10) {
    mod.exports = `data:image/${mod.filename
      .split('.')
      .pop()};base64,${fs.readFileSync(mod.filename, {
      encoding: 'base64'
    })}`;
    return ignoreStyles.noOp();
  }

  // If we find an image
  const hash = md5File.sync(filename).slice(0, 8);
  const bn = path.basename(filename).replace(/(.w{3})$/, `.${hash}$1`);

  mod.exports = `/static/media/${bn}`;
});

// Set up babel to do its thing... env for the latest toys, react-app for CRA
// Notice three plugins: the first two allow us to use import rather than require, the third is for code splitting
// Polyfill is required for Babel 7, polyfill includes a custom regenerator runtime and core-js
require('@babel/polyfill');
require('@babel/register')({
  ignore: [//(build|node_modules)//],
  presets: ['@babel/preset-env', '@babel/preset-react'],
  plugins: [
    '@babel/plugin-syntax-dynamic-import',
    '@babel/plugin-proposal-class-properties',
    'dynamic-import-node',
    'react-loadable/babel'
  ]
});

// Now that the nonsense is over... load up the server entry point
const app = require('./server');
module.exports = app;

Then I have the regular express server in my server.js

// Express requirements
import bodyParser from 'body-parser';
import compression from 'compression';
import express from 'express';
import morgan from 'morgan';
import path from 'path';
import forceDomain from 'forcedomain';
import Loadable from 'react-loadable';
import cookieParser from 'cookie-parser';

// Our loader - this basically acts as the entry point for each page load
import loader from './loader';

// Create our express app using the port optionally specified
const main = () => {
  const app = express();
  const PORT = process.env.PORT || 3000;
  // Compress, parse, log, and raid the cookie jar
  app.use(compression());
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(morgan('dev'));
  app.use(cookieParser());

  // Set up homepage, static assets, and capture everything else
  app.use(express.Router().get('/', loader));
  const favicon = require('serve-favicon');
  app.use(favicon(path.resolve(__dirname, '../build/icons/favicon.ico')));
  app.use(express.static(path.resolve(__dirname, '../build')));
  app.use(loader);

  // We tell React Loadable to load all required assets and start listening - ROCK AND ROLL!
  Loadable.preloadAll().then(() => {
    app.listen(PORT, console.log(`App listening on port ${PORT}!`));
  });

  // Handle the bugs somehow
  app.on('error', error => {
    if (error.syscall !== 'listen') {
      throw error;
    }

    const bind = typeof PORT === 'string' ? 'Pipe ' + PORT : 'Port ' + PORT;

    switch (error.code) {
      case 'EACCES':
        console.error(bind + ' requires elevated privileges');
        process.exit(1);
        break;
      case 'EADDRINUSE':
        console.error(bind + ' is already in use');
        process.exit(1);
        break;
      default:
        throw error;
    }
  });

  return app;
};

module.exports = main();

Then, I have the loader do:

// Express requirements
import path from 'path';
import fs from 'fs';

// React requirements
import React from 'react';
import { renderToString } from 'react-dom/server';
import Helmet from 'react-helmet';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import { Frontload, frontloadServerRender } from 'react-frontload';
import Loadable from 'react-loadable';

// Our store, entrypoint, and manifest
import createStore from '../src/configureStore';
import App from '../src/containers/app';
import manifest from '../build/asset-manifest.json';

// Some optional Redux functions related to user authentication
//import { setCurrentUser, logoutUser } from '../src/modules/auth';

// LOADER
export default (req, res) => {
  /*
    A simple helper function to prepare the HTML markup. This loads:
      - Page title
      - SEO meta tags
      - Preloaded state (for Redux) depending on the current route
      - Code-split script tags depending on the current route
  */
  const injectHTML = (data, { html, title, meta, body, scripts, state }) => {
    data = data.replace('<html>', `<html ${html}>`);
    data = data.replace(/<title>.*?</title>/g, title);
    data = data.replace('</head>', `${meta}</head>`);
    data = data.replace(
      '<div id="root"></div>',
      `<div id="root">${body}</div><script>window.__PRELOADED_STATE__ = ${state}</script>${scripts.join(
        ''
      )}`
    );

    return data;
  };

  // Load in our HTML file from our build
  fs.readFile(
    path.resolve(__dirname, '../build/index.html'),
    'utf8',
    (err, htmlData) => {
      ...

The entire process, including transpilation, and the time necessary to start the express server takes quite a while. My latency could be anywhere between 100ms for a warm lambda, all the way to around 3 seconds.

Is there some low-hanging fruit improvements that I could apply to my code?

question from:https://stackoverflow.com/questions/65928878/how-could-i-reduce-the-long-cold-start-time-when-doing-ssr-from-lambda

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)
Waitting for answers

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...