rails / webpacker

Use Webpack to manage app-like JavaScript modules in Rails
MIT License
5.31k stars 1.47k forks source link

Weird loading order after upgrading to Webpack 4 #2016

Closed doutatsu closed 3 years ago

doutatsu commented 5 years ago

I've upgraded to webpack 4 for Rails. I use it with Vue.js 2. I also use chunks in my configuration. But since upgrading, I've noticed that the page load order is weird. The page loads HTML before styles and JS has been loaded, which is not what happened before. I've attached links to the videos for before and after to understand the issue better.

I've been looking in here and everywhere to find anyone with the same issue, but I couldn't...

With Webpack 3(before)

With Webpack 4(after)

As you can see, it starts of with just HTML, no css applied at all and then loads it bit by bit. ElementUI styles are applied at the very end, as can be seen from the Sign in button becoming styles...

Here are my configuration files:

Dev Config

const environment = require('./environment')
const BundleAnalyzerPlugin =
  require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

environment.plugins.append(
  'BundleAnalyzerPlugin',
  new BundleAnalyzerPlugin()
)

module.exports = environment.toWebpackConfig()

Env (shared) Config

const { environment } = require('@rails/webpacker')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const vue = require('./loaders/vue')

const additionalConfig = {
  plugins: [
    new VueLoaderPlugin(),
  ],
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
        vendor: {
          name: 'vendor',
          chunks: 'all',
          test: /[\\/]node_modules[\\/]/,
          minChunks: 3,
        },
      }
    }
  },
  module: {
    rules: [{
      test: /\.pug$/,
      loader: 'pug-plain-loader'
    }, {
      test: /\.sass$/,
      use: ['vue-style-loader', 'css-loader', 'sass-loader']
    }]
  },
  output: {
  },
  devtool: 'source-map',
}

environment.config.merge(additionalConfig);

environment.loaders.prepend('vue', vue)

module.exports = environment

Pack related to the page in the video

import 'element-ui/lib/theme-chalk/index.css';
import 'element-ui/lib/theme-chalk/display.css';
import 'flexboxgrid/css/flexboxgrid.css';

import Vue from 'vue/dist/vue.esm';
import VueCookies from 'vue-cookies';
import { DateTime } from 'luxon';

// ElementUI Components
import ElementUI from 'element-ui';
import locale from 'element-ui/lib/locale/lang/en';

// Custom Components
import TextSection from '../components/TextSection.vue';
import TopNavigation from '../components/navigation/TheTopNavigation.vue';

import { store } from '../store';

Vue.use(ElementUI, { locale });
Vue.use(VueCookies);

const app = new Vue({
  el: '#app',
  store,
  mounted() {
    var selector = document.querySelector("#app");
    var errors   = selector.dataset.errors;

    if (selector) {
      store.commit('base_states/authenticate',
        JSON.parse(selector.dataset.signedIn)
      );
    }

    if (errors) {
      this.$message({
        dangerouslyUseHTMLString: true,
        message: JSON.parse(errors).join("\n"),
        type: 'error'
      });
    }
  },
  components: { TextSection, TopNavigation },
});

if (!app.$cookies.get('timezone')) {
  app.$cookies.set("timezone", DateTime.local().zoneName);
}

Rails view for that page

#app{ data: { signed_in: "#{user_signed_in?}", errors: flash[:errors] } }
  .landing-top
    .row.banner
      %top-navigation{ ":user" => user, "logo" => logo }
      .row.start-sm.around-sm.middle-sm.center-xs.landing-hero
        .col-lg-4.col-md-4.col-sm-4.col-xs-12
          %h1= t 'static.banner.headline'
          %p= t 'static.banner.subtitle'
          .actions
            %a.no-decoration{ class: "el-button el-button--success", href: "/events" }
              See upcoming events
        .col-lg-6.col-md-6.col-sm-6.col-xs-12
          = video_tag("https://s3.eu-west-2.amazonaws.com/vras-assets/product_preview_new.webm",
                      poster: preview_poster,
                      class: "preview-video", autoplay: "true",
                      muted: "true",          loop: "true" )
  .landing-body.site-padding
    .row.around-lg.middle-lg.middle-md.features
      .col-md-4.col-xs-12.feature-column
        = inline_svg 'icons/potion.svg', class: 'svg-icon'
        %text-section{ "title" => t('static.first_section.title_one'),
                       "text"  => t('static.first_section.text_one') }
      .col-md-4.col-xs-12.feature-column
        = inline_svg 'icons/map.svg', class: 'svg-icon'
        %text-section{ "title" => t('static.first_section.title_two'),
                       "text"  => t('static.first_section.text_two') }
      .col-md-4.col-xs-12.feature-column
        = inline_svg 'icons/unicorn.svg', class: 'svg-icon'
        %text-section{ "title" => t('static.first_section.title_third'),
                       "text"  => t('static.first_section.text_third') }
  .row.center-lg.center-xs.video-showcase
    .col-lg-10.col-md-10.col-xs-12
      = video_tag('https://s3.eu-west-2.amazonaws.com/vras-assets/preview.mp4',
                  poster: 'meta_cover.jpg',
                  class: 'preview-video',
                  autoplay: 'true',
                  muted: 'true',
                  loop: 'true')
    .col-lg-8.col-md-8.col-xs-10{ style: "padding-top: 20px" }
      %h3
        = image_tag("bigscreen_logo.png", width: "250px")
        %br
        = t('static.third_section.title')
      %text-section{ "text"  => t('static.third_section.text') }
  .landing-body.site-padding
    .row.around-lg.middle-lg.middle-md{ style: "margin-bottom: 100px" }
      .col-lg-6.col-md-6.col-xs-12
        %text-section{ "title" => t('static.second_section.title'),
                       "text"  => t('static.second_section.text') }
      .col-lg-6.col-md-6.col-xs-12.first-xs.last-lg.last-md{ style: "text-align: center" }
        %iframe{:title => "Discord Widget", :allowtransparency => "true", :frameborder => "0", :height => "519", :src => "https://discordapp.com/widget?id=402246704252059648&theme=dark", :width => "320"}
  = render "footer"

= javascript_packs_with_chunks_tag 'landing_page'
= stylesheet_packs_with_chunks_tag 'landing_page'
doutatsu commented 5 years ago

My research led me to believe its this issue:

This is happening because you're bundling with style-loader, which puts your CSS as a string inside your Javascript bundle.

So the HTML will render (very fast) while the browser is parsing your JS bundle (very slow). Towards the end of that bundle, the browser will find the module containing your CSS string. Then it'll parse that and apply the styles with Javascript.

Following this, I came up with two solutions:

  1. I've extracted the CSS I need into Rails app/assets folder, to load outside of webpack and Vue. That's the critical CSS I wanted, like footer and header, and margin rules

  2. While I didn't quite get to the bottom of this, but I have found an interim simple solution to get back to the original webpack 3 behaviours, of JS/CSS blocking the loading of the page:

window.addEventListener('load', () => {
  ### Initialise Vue app
});

It forces to wait till window loads all the JS/CSS assets, before loading my Vue. This worked on all, but one page, as everything except landing page is built from Vue components only.

For landing page, I've adopted another suggestion, to delegate JS/CSS pack load to the head, with a yield. So now we are also blocking execution on the landing page as well. Combined, I am seeing exactly the same loading behaviour as before.

Hope this helps some of you here. The final solution I want to eventually do is to adopt a skeleton pattern, where I have only outline of my components to show and then they would fill out as the page loads.

jakeNiemiec commented 5 years ago

From the v4 upgrade guide, did you have extract_css set?:

Due to the change in #1625, you'll want to make sure that extract_css is set to true for the default environment in webpacker.yml if you want to have Webpacker supply your CSS.

Putting the extracted styles in the <head> is the ideal solution. If your JS is too fast, you can defer it with https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script.

You can also break things up with dynamic import so that things load faster due to http/2.

infantopratik commented 5 years ago

I was stuck with the same problem in my react app and used the following plugin to extract the CSS (as webpack 4 requires it). Apparently, this is a required step to add in your webpack config, which I had missed earlier. https://github.com/webpack-contrib/mini-css-extract-plugin

jakeNiemiec commented 5 years ago

@infantopratik, Webpacker should be automatically adding this plugin for you:

https://github.com/rails/webpacker/blob/54c3ca9245e9ee330f8ca63b447c202290f7b624/package/environments/base.js#L36-L42

doutatsu commented 5 years ago

From the v4 upgrade guide, did you have extract_css set?:

@jakeNiemiec I was aware of this and kept it as false in default, hence the development environment, just as the unedited webpacker.yml is. But of course it's set to true in production environment.

That's the reason why production never looked as bad as on development, but it still had a bit of ordering issue, which I fixed by adding that extra load and critical CSS.

I also do use chunks for more efficient caching.

@infantopratik as jake said, Webpack provides this by default and I didn't need to it manually myself. I also at some point thought I needed it, but looking into webpacker source, I saw that it gets added

guillaumebriday commented 3 years ago

Is this still an issue?