Minifying and Modularizing Assets in Yii2

Asset minification and modularization is typically a fast process in most projects but in Yii2, it is a different beast altogether. This is mostly due to the fact that Yii2 has a different way of loading and bundling assets when importing widgets and other libraries. In short, it can be a nightmare to bundle up and minify correctly.

Aleksandar Panic
Fullstack Developer

Why?

The main culprit is the AssetBundle class. You define the assets you need for your widget or site and then simply register the AssetBundle class in your view. Assets get loaded and you get your nice styles, themes and JS interactivity. Seems simple, right?

However, this class is not locked down. You can make your own implementations of this class - override all the functions, add new functions, implement your own way of asset path searching and even which assets to select. For normal use cases, this is a good thing. But for minification, this causes a lot of problems because of all the different function calls, language assets, etc. You might even have to face the possibility that you might not be able minify all assets completely.

But what about asset/compress?

Yes, according to documentation there is a way to do this. But it requires everybody to follow the rules - every library and every asset bundle must be defined strictly in terms of their $css, $js and $depends configurations. However, let’s say you have a bundle like MomentAsset, which implements loading different locale according to specified language in Yii2 - even if you manage to load a specified locale by yourself, this widget will still include the file even when you remove it using the guide provided in the documentation. Some specific widgets even have an addLanguage function implemented, which gets called by the widget so those files would still be added.

Also, what about $this->registerJs(), $this->registerCss() ? We want to remove that too, or at least minimize it to as few calls as possible. And for debugging? This is a nightmare. Since everything is minified, you have no idea where things originate from.

What we want to accomplish is to make it possible to have zero loaded assets from asset bundle classes and to allow for easy debugging.

So how do we do it?

In one of our projects, we had a task to minify and modularize assets into minified asset bundles. We approached this problem in multiple steps.

First, we created our own asset bundle which contained our minified assets. Its implementation looks like this:

<?php
class MinifiedAsset extends AssetBundle
{
	public $basePath = '@webroot';
	public $baseUrl = '@web';
 
    public $css = [
	    'build/styles.min.css',
	];
 
	public $js =  [
        'build/scripts.min.js'
	];
 
	public function init()
	{
    	parent::init();
 
    	if (Yii::$app->request->isAjax || Yii::$app->request->isPjax)
    	{
        	$this->js = [];
        	$this->css = [];
        	return;
    	}
	}
 
	public $depends = [];
}

This class will need to be registered, either in the main layout of your Yii2 application (which you should normally have) or in every main layout if you have multiple main layouts (such as in the case of different main application and login pages). The Ajax and Pjax request-checking is here so that these files are not loaded when any of the pjax/ajax views are rendered, such as grid view re-rendering.

Second, we created a dummy asset bundle class which we used to remap all other asset bundles to. We also added some specific functions to it, such as addLanguage, which essentially just removes the additional assets. That implementation looks like this:

<?php
class DummyAsset extends AssetBundle
{
	public $css = [];
 
	public $js = [];
 
	public $depends = [
        MinifiedAsset::class
	];
 
	public function addLanguage()
	{
    	// NOTE: This is an override for Kartik widget’s "fancy functions" in its own asset bundles. This is for example purposes to show how to handle other bad asset bundle implementations.
    	return $this;
	}
}

Third, we found all other asset bundle classes used by all of the widgets in our project and remapped them to this dummy asset bundle. This was the most tedious task as it involves searching the code of the widgets we used but we ended up with a good list of asset bundle classes to override.

Note that depending on how the widget uses the scripts, you might not be able to remap that asset bundle and will still need to continue using it. This is the time to decide whether that widget/library is worth keeping in your project.

Once everything is done, you will need to go to your configuration and remap all of the asset bundle classes to the DummyAsset.

So, the config would look like this:

<?php
return [
	'components' => [
    	'assetManager' => [
        	'bundles' => [
                'yii\grid\GridViewAsset' => ['class' => '\frontend\assets\DummyAsset'],
                'yii\jui\JuiAsset' => ['class' => '\frontend\assets\DummyAsset'],
         	   'frontend\assets\AppAsset' => ['class' => '\frontend\assets\DummyAsset'],
            	// ... and more
        	]
    	],
	]
];

The final step is to minify the assets into one file. Now that you got rid of all assets from the project, the next step is to minify them. You will need a bundler tool such as webpack, grunt, or gulp.

In our case, we decided that gulp worked best for our purposes. You will need to list all of the asset’s files in your gulpfile.js and run the gulp task to process the assets.

So, what about that registerJs and registerCss?

The next step was to modularize JS and CSS inside of views. In our project, we used registerJs() a lot to handle some dynamic tasks, which caused our views to get bloated and this needed to change. We also had some view-specific CSS, which we kept in the main CSS files, but we wanted it to be as close to the PHP files as possible. Hence, we ended up doing the following:

First, we created some JS logic for defining “view JS pages”. In your view PHP file (for this example, let it be myview.php), create a “wrapper” element with an attribute data-page-view=”my-unique-id”. This is what it looks like:

<?php
/**
 * My view PHP file
 * @var $this \yii\web\View
 */
?>
<div data-page-view="my-unique-id">
   All other view elements go here.
</div>
 
<?php
Yii::$app->scripts->configureView('my-unique-id', [
	'key' => 'value',
	'key2' => 'value2'
]);

Don’t worry about the configureView call yet. We will get to that.

Next, we created a JS file in the same folder as myview.js and we defined the following function in it:

MainApp.module.view.handle('my-unique-id', function MyView(config) {
 	// All JS for myview.php will be here.
});

MyView function will be invoked every time, for every wrapper element in the page with my-unique-id as its value and will pass the configuration from the view PHP file (specified by configureView function) to it, in its config parameter in MyView function.

Also, since we are running this per element, we can pass this element to the function as well. We passed it as this context of the function, so you can essentially do the following:

MainApp.module.view.handle('my-unique-id', function MyView(config) {
     this.find('a').click(function() {
     	alert('I am bound only to elements within this view!');
 	});
});

Once that was done, we needed a way to pass this configuration from the view's PHP file into the JS file, regardless of whether the view was loaded using standard page reload or the ajax reload. So, we ended up making a Scripts component. This component would run after the controller action and inject the small script, which will pass all of the configurations to all of the views and then render when it’s an ajax or a standard page reload but not on other response types, such as JSON responses.

This component will load in bootstrap phase of Yii2 request, so we don’t have to worry about whether it’s loaded or not.

And what about CSS? Well, this one’s easy. Since we used SCSS with gulp, we could accomplish nesting easily. So, we created a SCSS file in the view - myview.scss and in it, we simply did the following:

[data-page-view="my-unique-id"] {
	// Everything here is only for this view.
}

Additionally, we added views/**/*.js and views/**/*.scss to our gulpfile build list so that all of our JS and SCSS files are minified as well.

In conclusion, we separated asset concerns from views, essentially separating the code. And for debugging, there is Sourcemaps. We defined that in the gulpfile and all our files are resolved nicely to real source files when debugging, so we got that handled as well.

If you want to see this in action you can look at this Yii2 example project code: https://github.com/ArekX/yii2-asset-modularization-example