Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global javascripts #2039

Closed
lukaszwnek opened this issue Apr 1, 2019 · 6 comments
Closed

Global javascripts #2039

lukaszwnek opened this issue Apr 1, 2019 · 6 comments

Comments

@lukaszwnek
Copy link

Hi! First of all, thanks for this awesome tool.
I'm currently in the middle of upgrading from version 3 to 4 and bumped into the following problem.

Before the upgrade I had a Rails layout file which included a "global" pack using:
<%= javascript_pack_tag "global" %>

This pack included all the javascript that is used across all our views. Specifically, the layout file has a top bar menu with some JS dropdowns and whatnot. This global pack handled all that shared code. Then, every time we had some additional javascript that we needed, we would add it in the Rails view file (rendered within the layout file):

<%= javascript_pack_tag "data-table" %>

Not all views rendered within this specific layout had those additional packs, some of them were static Rails pages.

Now, while doing the upgrade, I wanted to start using splitChunks as a replacement for the deprecated CommonsChunkPlugin. Here's my current setup:

environment.splitChunks((config) => Object.assign({}, config, {
  optimization: {
    splitChunks: {
      chunks: "all",
      name: false
    },
    runtimeChunk: "single"
  }
}));

The thing is, I can't figure out how to proceed with this "global" pack I had before. If I add this:

<%= javascript_packs_with_chunks_tag "global" %> to the layout file, I can't have the second <%= javascript_packs_with_chunks_tag "data-table" %> in my view file, as this duplicates the chunks (as stated in the README). I tried pushing it all to the view file, but then I have to make sure I have a javascript_packs_with_chunks_tag in all view files (even those static ones) and we have a lot of those. This doesn't feel right, as it creates a lot of duplication just to handle some javascript components from the layout file.

I also tried javascript_packs_with_chunks_tag in the layout and javascript_pack_tag in the view when I need it, but this apparently doesn't execute the pack from the view file.

I thought about having only a single pack, including it using javascript_packs_with_chunks_tag in the layout and using webpack's dynamic imports to import the additional packs for specific views (e.g. based on the URL). This, however, requires quite a lot of changes, so wanted to confirm that's the only way before digging into that. Am I missing something? Is there a better way of handling this scenario?

Thanks again

@jakeNiemiec
Copy link
Member

I thought about having only a single pack, including it using javascript_packs_with_chunks_tag in the layout and using webpack's dynamic imports to import the additional packs for specific views

I have found that this works very well, see here for details: #1944 (comment)

See here for deeper discussion: #1835

@rossta
Copy link
Member

rossta commented Jun 7, 2020

Just to add to this discussion a bit late:

It's a good recommendation from @jakeNiemiec; loading lazy chunks on-demand on a per-view basis is one way to go.

The split chunks optimization is also still effective, I believe, for your use case @lukaszwnek. You get the benefit of better caching, since webpack will separate out vendor modules into shared chunks, and, if you're using an HTTP/2-enabled CDN, better parallelization across the added bundles for downloading content.

The problem you've described is something I think Webpacker should help handle. As it stands now, developers have to come up with a way to ensure a single *_with_chunks_tag is used.

The way I've solved this is with some extra helpers that allow you to "add chunks" to the javascript_packs_with_chunks_tag from individual views. This works because the view template is processed before the application layout.

module MyWebpackerHelper
  def add_javascript_packs(*packs)
    @additional_javascript_packs ||= []
    @additional_javascript_packs += packs
  end

  def add_stylesheet_packs(*packs)
    @additional_stylesheet_packs ||= []
    @additional_stylesheet_packs += packs
  end

  def javascript_packs
    ["global"] + (@additional_javascript_packs || [])
  end

  def stylesheet_packs
    ["global"] + (@additional_stylesheet_packs || [])
  end
end
<!-- app/views/layouts/application.html.erb renders each "*with_chunks_tag" once -->
<%= stylesheet_packs_with_chunks_tag *stylesheet_packs %>
<%= javascript_packs_with_chunks_tag *javascript_packs %>
<!-- app/views/posts/show.html.erb modifies the "*_packs" lists -->
<% add_stylesheet_packs "data-table %>
<% add_javascript_packs "data-table" %>

Another helpful change is to set runtime: 'single' for the Webpacker split chunks configuration. This is necessary when using multiple entrypoints per page.

environment.splitChunks((config) => ({...config, ...{ optimization: { runtimeChunk: 'single' }}}))

Hope that's helpful.

@jakeNiemiec
Copy link
Member

@rossta This is a response to both issues.


loading lazy chunks on-demand on a per-view basis is one way to go...The split chunks optimization is also still effective, I believe

From the docs:

optimization.splitChunks:
By default webpack v4+ provides new common chunks strategies out of the box for dynamically imported modules. See available options for configuring this behavior in the SplitChunksPlugin page.

If we continue to that page, we see:

By default [split-chunks-plugin] only affects on-demand chunks, because changing initial chunks would affect the script tags the HTML file should include to run the project.

Imported modules are initialized for each runtime chunk separately, so if you include multiple [packs] points on a page, beware of this behavior.

As a rule of thumb: Use exactly one [pack] for each HTML document.


All of the above is in preparation to address this:

The way I've solved this is with some extra helpers that allow you to "add chunks" to the javascript_packs_with_chunks_tag from individual views.

You aren't wrong in this solution, but it looks like you are doing more work than you need to be. It also cuts against the grain of both tree-shaking and splitChunks (as evidenced by micromanaging the runtimeChunk).

Let's move this out of the esoteric realm and into the practical. I can illustrate this specifically if you can post an example repo containing this or a static profile file from this (run webpack with these flags --profile --json > ../profile.json). We can also go back and forth over at https://discuss.rubyonrails.org/ if you prefer. I feel like if users can see examples of these concepts in action, it will go a long way to prevent the pain points that they have been experiencing in these issues.

@rossta
Copy link
Member

rossta commented Jun 8, 2020

Ok! I'm going to capture a few key points then we can move to https://discuss.rubyonrails.org.

  1. Examples

I've been using this repo as a resource to help explain concepts to folks. The master branch is a vanilla Rails 6 app with Webpacker. There are many example branches that I use to illustrate or demo fix and changes for specific concerns.

Feel free to create a branch or use an existing one such as this https://github.com/rossta/rails6-webpacker-demo/compare/example/react-multi-pack off of which to base discussion. The repo is already set up with the webpack bundle analyzer and that branch is in a state to show a before/after of splitChunks optimization for an app with multiple entry points.

  1. Lazy chunks vs Multiple entry points

Let's discuss the benefits/tradeoffs of both approaches. I agree with the benefits of lazy chunking (I've been advocating for them for a while) though they may require developers to consider async loading UX. Multple entrypoints may be more work in some ways, but they are also more consistent with what Rails developers have been using in Sprockets.

By default [split-chunks-plugin] only affects on-demand chunks

Just a quick point on this: the default Webpacker config overrides the webpack default by applying optimization to 'all' chunk types, including initial chunks.

chunks: 'all',

  1. More generally, how do we best educate the Rails community?

I think you and I are well-equipped to answer to these concerns and prescribe some patterns that would best serve the Webpacker community. I believe it's common for Rails developers to make certain assumptions about how things work in Webpacker (or expectations about how things should work) coming from the Rails asset pipeline. I've witnessed code- and bundle-splitting done wrong time and again with Webpacker so it would be great to have a set of examples to illustrate proper execution.

@h0jeZvgoxFepBQ2C
Copy link

h0jeZvgoxFepBQ2C commented Aug 25, 2020

Is there any reason against making javascript_packs_with_chunks_tag set an instance variable during the request, which tracks that chunks are included only once, even when called multiple times?

This would be the easiest way imho.. All other ways are much more complex and I really don't see any benefit of having javascript_packs_with_chunks_tag outputting multiple chunks?

@rossta
Copy link
Member

rossta commented Jan 13, 2021

@h0jeZvgoxFepBQ2C I would gladly support a PR in this fashion in the interest of improving developer ergonomics.


Looks like several promising solutions to the original issue have been posted.

@rossta rossta closed this as completed Jan 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants