react-rails icon indicating copy to clipboard operation
react-rails copied to clipboard

SplitChunks and SSR: Cannot read property 'serverRender' of undefined

Open RyanMcDonald opened this issue 5 years ago • 14 comments

Steps to reproduce

  • Enable SplitChunks.
  • Try server-rendering a component.

Expected behavior

The component should be rendered on the page.

Actual behavior

  • Rails was throwing the error: ReferenceError: window is not defined
    • I got past this by setting Webpack's globalObject:
      environment.config.set(
        'output.globalObject',
        "(typeof self !== 'undefined' ? self : this)"
      );
      
  • Now Rails is throwing the error: #<ExecJS::ProgramError: TypeError: Cannot read property 'serverRender' of undefined>
Stack trace
Encountered error "#<ExecJS::ProgramError: TypeError: Cannot read property 'serverRender' of undefined>" when prerendering ssr/Button with {}
eval (eval at <anonymous> ((execjs):147:8), <anonymous>:6:45)
eval (eval at <anonymous> ((execjs):147:8), <anonymous>:18:13)
(execjs):147:8
(execjs):153:14
(execjs):1:102
Object.<anonymous> ((execjs):1:120)
Module._compile (internal/modules/cjs/loader.js:738:30)
Object.Module._extensions..js (internal/modules/cjs/loader.js:749:10)
Module.load (internal/modules/cjs/loader.js:630:32)
tryModuleLoad (internal/modules/cjs/loader.js:570:12)
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/execjs-2.7.0/lib/execjs/external_runtime.rb:39:in `exec'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/execjs-2.7.0/lib/execjs/external_runtime.rb:21:in `eval'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/server_rendering/exec_js_renderer.rb:39:in `render_from_parts'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/server_rendering/exec_js_renderer.rb:20:in `render'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/server_rendering/bundle_renderer.rb:40:in `render'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/server_rendering.rb:27:in `block in render'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/connection_pool-2.2.2/lib/connection_pool.rb:65:in `block (2 levels) in with'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/connection_pool-2.2.2/lib/connection_pool.rb:64:in `handle_interrupt'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/connection_pool-2.2.2/lib/connection_pool.rb:64:in `block in with'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/connection_pool-2.2.2/lib/connection_pool.rb:61:in `handle_interrupt'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/connection_pool-2.2.2/lib/connection_pool.rb:61:in `with'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/server_rendering.rb:26:in `render'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/rails/component_mount.rb:67:in `prerender_component'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/rails/component_mount.rb:34:in `block in react_component'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.1.6.1/lib/action_view/helpers/capture_helper.rb:39:in `block in capture'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.1.6.1/lib/action_view/helpers/capture_helper.rb:203:in `with_output_buffer'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.1.6.1/lib/action_view/helpers/capture_helper.rb:39:in `capture'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.1.6.1/lib/action_view/helpers/tag_helper.rb:272:in `content_tag'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/rails/component_mount.rb:50:in `react_component'
/Users/ryan.mcdonald/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/react-rails-2.4.7/lib/react/rails/view_helper.rb:21:in `react_component'

System configuration

Sprockets or Webpacker version: Webpacker 4.0.2 React-Rails version: 2.4.7 Rect_UJS version: 2.4.4 Rails version: 5.1 Ruby version: 2.5.1


It works perfectly fine when SplitChunks is not enabled. There is something happening where the ReactRailsUJS object is being lost on the server, so this line fails.

This is the SplitChunks config that I'm using:

optimization: {
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: 3,
      maxAsyncRequests: 5,
      minSize: 30000,
      name: false,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          minSize: 30000,
          enforce: true,
        },
      },
    },
    runtimeChunk: true,
  },

RyanMcDonald avatar Mar 13 '19 16:03 RyanMcDonald

I've created a repo using a fresh Rails 5.1.6.2 install that reproduces the issue using SplitChunks defaults: https://github.com/RyanMcDonald/react-rails-ssr-webpacker-splitchunks/blob/master/config/webpack/environment.js

RyanMcDonald avatar Mar 13 '19 22:03 RyanMcDonald

I found the issue and updated my sample repo with the solution that I found.

It looks like react-rails's default WebpackerManifestContainer calls Webpacker.manifest.lookup instead of Webpacker.manifest.lookup_pack_with_chunks when SplitChunks is enabled, so it never ends up executing the server_rendering.js which it needs in order to set the ReactRailsUJS global variable.

WebpackerManifestContainer should probably be updated to account for SplitChunks.

RyanMcDonald avatar Mar 25 '19 19:03 RyanMcDonald

A few people have ran into issues with CommonsChunkPlugin or SplitChunksPlugin, I'll likely have to do something to some detection for both depending on which module is loaded but this is a really helpful tip. :+1: Thanks @RyanMcDonald this is the tip-off I needed.

Edit: Related to https://github.com/reactjs/react-rails/issues/863

BookOfGreg avatar Jun 08 '19 13:06 BookOfGreg

Thank you for reporting this, it helped me debug my application.

ericraio avatar Jun 08 '19 19:06 ericraio

I configured SplitChunks to ignore server_rendering.js, which seems to work fine:

// We have two entry points, application and server_rendering.
// We always want to keep server_rendering intact, and must
// exclude it from being chunked.

const notServerRendering = name => name !== 'server_rendering';

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks(chunk) {
            return notServerRendering(chunk.name);
          }
        }
      }
    }
  }
};

RiccardoMargiotta avatar Jun 10 '19 09:06 RiccardoMargiotta

@RiccardoMargiotta

When importing the chunks in your rails layout, do you still use the javascript_packs_with_chunks_tag?

ericraio avatar Jun 10 '19 15:06 ericraio

We don't have that many chunks yet, mainly just an app and vendor chunk, along with a few used only in specific views. So we're just using standard pack tags for each one.

RiccardoMargiotta avatar Jun 10 '19 18:06 RiccardoMargiotta

@ericraio I checked and yes you can still use javascript_packs_with_chunks_tag. What's happening is that the SSR will only use server_rendering.js to execute javascript on the page, and all of the chunked javascript files are ignored on the server side. That way, you still get the efficiency of splitChunks for delivering assets to the client.

schuylr avatar Jun 20 '19 13:06 schuylr

I tried the solutions mentioned here with a fresh Rails 6 app and nothing helped. Is there any movement on this?

brandoncc avatar Jan 01 '20 09:01 brandoncc

Same here. Rails 6. Neither monkey-patch nor chunk exclusion helped...

gencer avatar Jan 22 '20 15:01 gencer

I configured SplitChunks to ignore server_rendering.js, which seems to work fine:

// We have two entry points, application and server_rendering.
// We always want to keep server_rendering intact, and must
// exclude it from being chunked.

const notServerRendering = name => name !== 'server_rendering';

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks(chunk) {
            return notServerRendering(chunk.name);
          }
        }
      }
    }
  }
};

This worked for me, too! But why does it work?

UPDATE: If you opt-out the server_rendering, it definitely adds/duplicates your vendor files to the server_rendering.js... Not sure if that's ideal.

andrefox333 avatar Mar 11 '20 20:03 andrefox333

If you opt-out the server_rendering, it definitely adds/duplicates your vendor files to the server_rendering.js... Not sure if that's ideal.

My understanding was that server_rendering.js needs to have vendor files included to function, presumably so it can render components with React DOM. Since that's only used by the server and not shipped to clients, I haven't been too worried about it. Although I don't know if there are performance considerations, I suppose ultimately the server still needs to run that JS...


I mentioned on the other post, I have an updated version of that snippet to also create a separate vendor_react bundle to allow for longer caching. The concept is still the same, blocking server_rendering.js from being chunked.

We still just have one large application bundle, though. In future, I'd like to at least route-split the app JS with additional entry points, but haven't gotten that working with the react-rails gem yet.

// We have two entry points, application and server_rendering.
// We always want to keep server_rendering intact, and must
// exclude it from being chunked.

const notServerRendering = name => name !== 'server_rendering';

module.exports = {
  optimization: {
    splitChunks: {
      chunks(chunk) {
        return notServerRendering(chunk.name);
      },
      cacheGroups: {
        vendor: {
          name: 'vendor',
          priority: -10,
          test: /[\\/]node_modules[\\/]/
        },
        vendor_react: {
          name: 'vendor_react',
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/
        }
      }
    }
  }
};
  <%= javascript_pack_tag "vendor_react.js", defer: true %>
  <%= javascript_pack_tag "vendor.js", defer: true %>
  <%= javascript_pack_tag "application.js", defer: true %>

RiccardoMargiotta avatar Mar 12 '20 09:03 RiccardoMargiotta

Hey @RyanMcDonald, thanks so much for your example and the repro! Between the two, I got this working as well.

For anyone else who attempts this next, one other piece that I needed to do was add this:

// environment.js
environment.config.set(
  'output.globalObject',
  "(typeof self !== 'undefined' ? self : this)"
)

Otherwise you'll get a window is not defined error.

ksweetie avatar Mar 27 '20 07:03 ksweetie

I was not able to get the javascript_packs_with_chunks_tag to work with any solution provided here. The page will render, but the packages are not split into chunks as they would be if you just turned off server rendering.

oktalk avatar Jun 08 '20 22:06 oktalk

As per the above conversations, we have a resolution provided by @RyanMcDonald. Closing the issue.

alkesh26 avatar Nov 04 '22 06:11 alkesh26