Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Single Runtime Chunk

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Head back to the homepage and click any of the articles. In an earlier tutorial, we added this heart icon that, when you click it, makes an AJAX request and increases the counter. Well, part of this is faked on the backend, but you get the idea.

To make this more clear, let's add a Bootstrap tooltip: when the user hovers over the heart, we can say something like "Click to like". No problem: open up the template: article/show.html.twig. And I'll remind you that this page has its own entry: article_show.js:

... lines 1 - 80
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('article_show') }}
{% endblock %}
... lines 86 - 92

Go open that: assets/js/article_show.js.

Ok, let's find the anchor tag in the template... there it is... and use multiple lines for sanity. Now add title="Click to Like":

... lines 1 - 4
{% block content_body %}
<div class="row">
<div class="col-sm-12">
... line 8
<div class="show-article-title-container d-inline-block pl-3 align-middle">
... lines 10 - 15
<span class="pl-2 article-details">
... line 17
<a href="{{ path('article_toggle_heart', {slug: article.slug}) }}" class="fa fa-heart-o like-article js-like-article" title="Click to Like!"></a>
</span>
... lines 20 - 24
</div>
</div>
</div>
... lines 28 - 78
{% endblock %}
... lines 80 - 92

To make this work, all we need to do is copy the js-like-article class, go back to article_show.js and add $('.js-like-article').tooltip(), which is a function added by Bootstrap:

... lines 1 - 3
$(document).ready(function() {
$('.js-like-article').tooltip();
... lines 6 - 19
});

Coolio! Let's try it. Refresh and... of course. It doesn't work:

...tooltip is not a function

This may or may not surprise you. Think about it: at the bottom of the page, the app.js <script> tags are loaded first. And, if you remember, inside of app.js, we import jquery and then bootstrap, which adds the tooltip() function to jQuery:

26 lines assets/js/app.js
... lines 1 - 10
import $ from 'jquery';
import 'bootstrap'; // adds functions to jQuery
... lines 13 - 26

Are Modules Shared across Entries?

So, it's reasonable to think that, inside article_show.js, when we import jquery, we will get the same jQuery object that's already been modified by bootstrap. And... that's almost true. When two different files import the same module, they do get the exact same object in memory.

However, by default, Webpack treats different entrypoints like totally separate applications. So if we import jquery from app.js and also from get_nice_message.js, which is part of the same entry:

26 lines assets/js/app.js
... lines 1 - 10
import $ from 'jquery';
import 'bootstrap'; // adds functions to jQuery
... lines 13 - 14
import getNiceMessage from './components/get_nice_message';
... lines 16 - 26

They will get the same jQuery object. But when we import jquery from article_show.js, we get a different object in memory. Each entrypoint has an isolated environment. It doesn't mean that jQuery is downloaded twice, it just means that we are given two different instances.

So the fix is simple: import 'bootstrap'.

Refresh and... this time, it works.

enableSingleRuntimeChunk()

Understanding that modules are not shared across entries is good to know. But this also relates to a feature I want to talk about: the runtime chunk.

In webpack.config.js, at the very beginning of the tutorial, we commented out enableSingleRuntimeChunk() and replaced it with disableSingleRuntimeChunk():

... lines 1 - 2
Encore
... lines 4 - 30
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
//.enableSingleRuntimeChunk()
.disableSingleRuntimeChunk()
... lines 35 - 76
;
... lines 78 - 79

Now, let's reverse that:

... lines 1 - 2
Encore
... lines 4 - 30
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
//.disableSingleRuntimeChunk()
... lines 35 - 76
;
... lines 78 - 79

Because we just modified the Webpack config, come back over, press Control + C and restart it:

yarn watch

If you watch closely, you'll see an immediate difference. Every single entry now includes a new file called runtime.js, which means that it's a new file that needs to be included as the first script tag before any entry. Of course, that's not a detail that we need to worry about because, when we refresh and view the page source, our Twig functions took care of rendering everything.

Ok, so... why? What did this change and why did we care? There are two things.

Single Runtime Chunk & Caching

First, runtime.js contains Webpack's "runtime" code: stuff it needs to get its job done. By enabling the single runtime chunk you're saying:

Hey Webpack! Instead of adding this code at the beginning of app.js and at the beginning of article_show.js and all my other entry files, only add it once to runtime.js

The user now has to download an extra file, but all the entry files are a bit smaller. But, there's more to it than that. The runtime.js file contains something called the "manifest", which is a fancy name that Webpack gives to code that contains some internal IDs that Webpack uses to identify different parts of your code. The key this is that those IDs often change between builds. So, by isolating that code into runtime.js, it means that our other JavaScript files - the ones that contain our big code - will change less often: when those internal IDs change, it will not affect their content.

The tl;dr is that the smaller runtime.js will change more often, but our bigger JavaScript files will change less often. That's great for caching.

Shared Runtime/Modules

The other thing that enableSingleRuntimeChunk() changes may or may not be a good thing. Go back to article_show.js and comment out import 'bootstrap'. Now, move over and refresh.

Yea, it works! When you enable the single runtime chunk, it has a side effect: modules are shared across your entry points: they all work a bit more like one, single application. That's not necessarily a good or bad thing: just something to be aware of. I still do recommend treating each entry file like its own independent environment, even if there is some sharing.

Next: it's time to talk about async imports! Have some code that's only used in certain situations? Make your built files smaller by loading it... effectively, via AJAX.

Leave a comment!

6
Login or Register to join the conversation
Mina-R Avatar
Mina-R Avatar Mina-R | posted 2 months ago | edited

hello i am configuring the enableSingleRuntimeChunk() in the webpack.config.js but when i used it with the tooltip component from bootstrap
to make a tooltip in other entry about.js and the importing of the tooltip component is on the app.js the tooltip is not working, the tooltip is not working in about page i dont know why but as far as i know that enabling this feature shares the tooltip comp. between all of the entry points so why it doesn't work:

// app.js
/*
 * Welcome to your app's main JavaScript file!
 *
 * We recommend including the built version of this JavaScript file
 * (and its CSS file) in your base layout (base.html.twig).
 */


// any CSS you import will output into a single css file (app.scss in this case)



import './styles/app.scss';
import 'bootstrap'
import setDefaultFlag from "./js/setDefaultFlag";
import {Tooltip} from "bootstrap";

import $ from "jquery";

document.querySelectorAll('.dropdown_item_select').forEach((dropDown,key) => {

    dropDown.addEventListener('click', function() {
        let imagePath = this
            .children
            .item(dropDown[key])
            .children
            .item(dropDown[key])
            .getAttribute('src');
        document.querySelector('#dropdown_button').innerHTML =
            setDefaultFlag(imagePath,dropDown.textContent);
    });
});

and

// about.js
import $ from 'jquery';
import './styles/about.scss';
const My_tooltip =  $('#click_me');

const tooltip = new Tooltip(My_tooltip);
tooltip.setContent({ '.tooltip-inner': 'another title' })
Reply

Hey @Mina-R,

yes it will be shared in compiled code, but in source code you still need to import it.

Cheers!

Reply
Default user avatar

Hello I have a question regarding "The runtime.js file contains something called the "manifest", which is a fancy name that Webpack gives to code that contains some internal IDs that Webpack uses to identify different parts of your code."

I was looking into my own webpack configuration and from what I understand, the runtime.js generated doesn't seem to be change and the ID that might change would be in the chunks that I generate. If that's the case, would that mean the benefit of not building our big .js files as often is incorrect? Thanks!

Reply

Hey Josh!

Yea... this stuff is super complicated - I've spent a lot of time looking into it for Encore... but it's also something that has changed over time :). So, you're correct that what's changing is the "id" that is used for each chunk. There are two important things to consider for this:

1) For a specific chunk, the id shouldn't change unnecessarily - like it shouldn't be called "1" and then suddenly "2" later. If I remember correctly, we use something in Encore (in production - it IS different in dev) to generate these in a static way so that they only change (I think) when the chunk filename changes.

2) When chunk ids do change (or new chunks are added) the second question is: in which file is this reflected? If we add a few new chunks to the system, we want those changes (ideally) to live in the small runtime.js and not big.js

Based on all of this, let me know if you're seeing something that doesn't make sense in your app.

Cheers!

1 Reply
seb-jean Avatar
seb-jean Avatar seb-jean | posted 4 years ago

When you said "I still do recommend treating each entry file like its own independent environment, even if there is some sharing.", do you recommend never using "enableSingleRuntimeChunk ()"?

Reply

Hey Sébastien J.

To the contrary, Ryan recommends to enable it but to be aware of the code sharing.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great with Symfony5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.91.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.3.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.5
        "symfony/console": "^4.0", // v4.2.5
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.5
        "symfony/framework-bundle": "^4.0", // v4.2.5
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.5
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.5
        "symfony/validator": "^4.0", // v4.2.5
        "symfony/web-server-bundle": "^4.0", // v4.2.5
        "symfony/webpack-encore-bundle": "^1.4", // v1.5.0
        "symfony/yaml": "^4.0", // v4.2.5
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.5
        "symfony/dotenv": "^4.0", // v4.2.5
        "symfony/maker-bundle": "^1.0", // v1.11.5
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.5
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.5
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.27.0", // 0.27.0
        "autocomplete.js": "^0.36.0",
        "autoprefixer": "^9.5.1", // 9.5.1
        "bootstrap": "^4.3.1", // 4.3.1
        "core-js": "^3.0.0", // 3.0.1
        "dropzone": "^5.5.1", // 5.5.1
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.4.0", // 3.4.0
        "popper.js": "^1.15.0",
        "postcss-loader": "^3.0.0", // 3.0.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.1", // 7.3.1
        "sortablejs": "^1.8.4", // 1.8.4
        "webpack-notifier": "^1.6.0" // 1.7.0
    }
}
userVoice