Options for CSS and JS Bundling and Minification with ASP.NET Core
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.
If getting a prompt like this bugs you, you can turn all prompting off here:
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:
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.
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!
Now you'll get a gulpfile.js that uses the bundleconfig.json and you've got full control:
And during the conversion you'll get the npm packages you need to do the work automatically:
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.
About Newsletter
bundling and minification of the JSS and CSaccidentally minified one S from CSS and bundled it into JS
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).
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.
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.
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.
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.
It's really tiny: BasicBundles at https://github.com/jtheisen/basic-bundles
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.
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).