Scott Hanselman

Options for CSS and JS Bundling and Minification with ASP.NET Core

March 18, 2017 Comment on this post [15] Posted in ASP.NET | DotNetCore
Sponsored By

Maria and I were updating the NerdDinner sample app (not done yet, but soon) and were looking at various ways to do bundling and minification of the JSS and CS. There's runtime bundling on ASP.NET 4.x but in recent years web developers have used tools like Grunt or Gulp to orchestrate a client-side build process to squish their assets. The key is to find a balance that gives you easy access to development versions of JS/CSS assets when at dev time, while making it "zero work" to put minified stuff into production. Additionally, some devs don't need the Grunt/Gulp/npm overhead while others absolutely do. So how do you find balance? Here's how it works.

I'm in Visual Studio 2017 and I go File | New Project | ASP.NET Core Web App. Bundling isn't on by default but the configuration you need IS included by default. It's just minutes to enable and it's quite nice.

In my Solution Explorer is a "bundleconfig.json" like this:

// Configure bundling and minification for the project.
// More info at https://go.microsoft.com/fwlink/?LinkId=808241
[
{
"outputFileName": "wwwroot/css/site.min.css",
// An array of relative input file paths. Globbing patterns supported
"inputFiles": [
"wwwroot/css/site.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
// Optionally specify minification options
"minify": {
"enabled": true,
"renameLocals": true
},
// Optionally generate .map file
"sourceMap": false
}
]

Pretty simple. Ins and outs. At the top of the VS editor you'll see this yellow prompt. VS knows you're in a bundleconfig.json and in order to use it effectively in VS you grab a small extension. To be clear, it's NOT required. It just makes it easier. The source is at https://github.com/madskristensen/BundlerMinifier. Slip this UI section if you just want Build-time bundling.

BundleConfig.json

If getting a prompt like this bugs you, you can turn all prompting off here:

Tools Options HTML Advanced Identify Helpful Extensions

Look at your Solution Explorer. See under site.css and site.js? There are associated minified versions of those files. They aren't really "under" them. They are next to them on the disk, but this hierarchy is a nice way to see that they are associated, and that one generates the other.

Right click on your project and you'll see this Bundler & Minifier menu:

Bundler and Minifier Menu

You can manually update your Bundles with this item as well as see settings and have bundling show up in the Task Runner Explorer.

Build Time Minification

The VSIX (VS extension) gives you the small menu and some UI hooks, but if you want to have your bundles updated at build time (useful if you don't use VS!) then you'll want to add a NuGet package called BuildBundlerMinifier.

You can add this NuGet package SEVERAL ways. Which is awesome.

  • Add it from the Manage NuGet Packages menu
  • Add it from the command line via "dotnet add package BuildBundlerMinifier"
    • Note that this adds it to your csproj without you having to edit it! It's like "nuget install" but adds references to projects!  The dotnet CLI is lovely.
  • If you have the VSIX installed, just right-click the bundleconfig.json and click "Enable bundle on build..." and you'll get the NuGet package.
    Enable bundle on build

Now bundling will run on build...

c:\WebApplication8\WebApplication8>dotnet build
Microsoft (R) Build Engine version 15
Copyright (C) Microsoft Corporation. All rights reserved.

Bundler: Begin processing bundleconfig.json
Bundler: Done processing bundleconfig.json
WebApplication8 -> c:\WebApplication8\bin\Debug\netcoreapp1.1\WebApplication8.dll

Build succeeded.
0 Warning(s)
0 Error(s)

...even from the command line with "dotnet build." It's all integrated.

This is nice for VS Code or users of other editors. Here's how it would work entirely from the command prompt:

$ dotnet new mvc
$ dotnet add package BuildBundlerMinifier
$ dotnet restore
$ dotnet run

Advanced: Using Gulp to handle Bundling/Minifying

If you outgrow this bundler or just like Gulp, you can right click and Convert to Gulp!

Convert to Gulp

Now you'll get a gulpfile.js that uses the bundleconfig.json and you've got full control:

gulpfile.js

And during the conversion you'll get the npm packages you need to do the work automatically:

npm and bower

I've found this to be a good balance that can get quickly productive with a project that gets bundling without npm/node, but I can easily grow to a larger, more npm/bower/gulp-driven front-end developer-friendly app.


Sponsor: Did you know VSTS can integrate closely with Octopus Deploy? Join Damian Brady and Brian A. Randell as they show you how to automate deployments from VSTS to Octopus Deploy, and demo the new VSTS Octopus Deploy dashboard widget. Register now!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
March 18, 2017 8:34
I've been playing with Webpack recently, which seems pretty good. There's a Webpack Task Runner extension for Visual Studio.

However, the Webpack Dev Server npm package doesn't seem to work well with dotnet CLI (or, I haven't figured out the best way to do the save to build functionality with ASP.Net Core).
March 18, 2017 9:20
bundling and minification of the JSS and CS
accidentally minified one S from CSS and bundled it into JS
March 18, 2017 9:44
What was the reason that MS didn't stick with the bundling solution of 4.x Versions? Imho it's still surperior to the gulp stuff etc..
March 18, 2017 16:10
I wonder how much longer bundling will be required with HTTP/2. Hopefully minification will be all we require soon.
March 18, 2017 21:50
@Maarten, probably because the old way only occurs at run time, and almost all newer methods occur at build time, which is much more efficient.
March 19, 2017 13:48
Any idea Scott why MS didn't stick bundling solution of 4.x Versions?
March 20, 2017 13:45
@Maarten The problem with the runtime minify & bundling is that it's a hit (be it small it's still a hit) on your server to produce. Whereas at build time you take the hit and then it's done.

The biggest problem I've found is that if the CSS/JS causes a minify error (but is valid CSS/JS), you don't get warned about it, it's only if you go to browse the location the minify bundle should be that you then see the error message and the un-minified file. I've had a production site running where I thought it was all working and minified and it was only by chance I noticed that it wasn't working when I viewed the source (and then was able to fix it).
March 20, 2017 17:56
Since I use NancyFX in Visual Studio, I had to find my own way. It's actually simple if you're willing to edit a solution file. I use Typescript so I just allow TS to concat all my JS into app.js. I also embed CSS in JS so I don't need CSS minification but it's easy to add if required.

Here's an example:


</PropertyGroup>
<Import Project="$(SolutionDir)packages\AjaxMin.5.14.5506.26202\tools\net40\AjaxMin.targets" />
<Target Name="ConcatenateVendorScripts" AfterTargets="AfterBuild" Condition="$(NCrunch) != '1'">
<ItemGroup>
<ScriptFiles Include="js\node_modules\mithril\mithril.min.js" />
<ScriptFiles Include="js\node_modules\vex-js\dist\js\vex.combined.min.js" />
</ItemGroup>
<ItemGroup>
<VendorScriptFileContents Include="$([System.IO.File]::ReadAllText(%(ScriptFiles.Identity)))" />
</ItemGroup>
<WriteLinesToFile File="Content\vendor.min.js" Lines="@(VendorScriptFileContents)" Overwrite="true" />
</Target>
<Target Name="ConcatenateVendorCss" AfterTargets="AfterBuild" Condition="$(NCrunch) != '1'">
<ItemGroup>
<CssFiles Include="css\vendor\pure-min.css" />
<CssFiles Include="js\node_modules\vex-js\dist\css\vex.css" />
<CssFiles Include="js\node_modules\vex-js\dist\css\vex-theme-plain.css" />
</ItemGroup>
<ItemGroup>
<VendorCssFileContents Include="$([System.IO.File]::ReadAllText(%(CssFiles.Identity)))" />
</ItemGroup>
<WriteLinesToFile File="Content\vendor.min.css" Lines="@(VendorCssFileContents)" Overwrite="true" />
</Target>
<Target Name="Minify" AfterTargets="AfterBuild" Condition="$(NCrunch) != '1' And $(ConfigurationName) == 'Release'">
<AjaxMin JsSourceFiles="Content\app.js" JsSourceExtensionPattern="\.js$" JsTargetExtension=".js" />
</Target>


AjaxMin is an MS package. It includes an MS build task which makes it easy to invoke from the solution file.
March 20, 2017 18:37
I will have to look into using VS 2017 bundling for .NET Core apps. I may use it for smaller sites, but for more complicated apps I will probably stick with Gulp. Gulp integration with Visual Studio has worked very well for me. A few dozen lines of javascript handles all of my pre-processing/bundling/minification/cache-busting/etc.

Additionally, some devs don't need the Grunt/Gulp/npm overhead while others absolutely do.
Between the gulp-watch and gulp-newer plugins, the overhead has been negligible for me. The tasks run only when source files are modified, and only the specific tasks that have modified files are run. It helps that I segment my javascript into a half-dozen contextual bundles for different areas in the site, with one or two common bundles used everywhere.
March 21, 2017 12:32
We are still working with the older sibling - MVC5 + ASP.Net 4.5.2

We use a nuGet package called Combres. It has an XML settings file in App_Data that defines groups of CSS or JS and whether or not each of those files is minified or not.

There is a .debug and .release version of the XML file.
So whilst developing the web.debug.config refs the debug Combres file but for production when the web.config is transformed using the web.release.config it uses the release Combres file. Thus when implementing stuff we get the unminified version and on production we get the minified/bundled versions.

In the view we can then simple say something like @Html.CombresLink("[name of bundle from config]") which will output the URL (that includes a timestamp). So whilst there might be a small hit at runtime if we ever have to hotfix a JS file at least we know browsers won't cache it because when the file gets overwritten the next request will generate a URL with a different timestamp.
March 21, 2017 12:40
Can anyone recommend some links to learning resources (or links to books on the subject that you'd recommend) on ASP.Core? I kinda prefer books because I find it easier to refer back to them and because they are usually more indepth, but at this point anything is going to be useful.

For me it's effectively like going from WinForms to WPF, everything I knew about web.configs/IIS/MVC has been ripped up and thrown away like yesterdays garbage so I need to get a handle on these things from the ground up.

For me though I work on IIS/Windows end-to-end so whilst potentially interesting, getting productive quicker is the main thing. I don't care about VS Code, various different web servers or any of that, I'll be on VS 2017, IIS, Windows 10.
March 21, 2017 19:14
The Bundler options is preferable in my opinion becuase I can make my build in VS work the same as my CI build. The Bundler as an option to integrate directly with MSBuild. The Task runner option with Gulp binds to VS commands for execution and not MSBuild so I have to go through extra steps for my CI to work well.
March 22, 2017 23:32
Drastic need for a way to catch invalid routes, invalid parameter types, etc at the ASP.NET MVC controller level and not at the web application level.
March 23, 2017 11:54
I have written a very small library that fills the need for versioning that you still have when bundling at compile time: You want your bundles to have a hash and the client to update in case of changes. In SPAs, you also want to detect such changes so that you know when a physcial page refresh becomes necessary.

It's really tiny: BasicBundles at https://github.com/jtheisen/basic-bundles
March 30, 2017 1:36
@Stephen Jones @Maarten I'm also of the opinion that runtime bundling is a far superior solution. Checking release runtime bundling is a simple process and the server hit is negligible in the grand scheme of things.

Where runtime bundling shines is simple cache busting, superior dev experience and support for https/2 ( better to supply individual files than 1 huge asset - you can detect at runtime )

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.