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>
... lines 20 - 24
... 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() {
... 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.


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
... 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
... lines 35 - 76
... lines 78 - 79

Now, let's reverse that:

... lines 1 - 2
... 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
... 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!

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