捆绑和缩小
作者: 里克·安德森
捆绑和缩小是两种可在 ASP.NET 4.5 中使用的方法来改进请求加载时间。 捆绑和缩小可以通过减少对服务器的请求数并减少所请求资产的大小(如 CSS 和 JavaScript)来提高加载时间。
大多数当前主要浏览器将每个主机名同时连接数限制为 6 个。 这意味着,在处理六个请求时,主机上资产的其他请求将由浏览器排队。 在下图中,IE F12 开发人员工具网络选项卡显示示例应用程序的“关于”视图所需的资产的时间。
灰色条形图显示等待六个连接限制的浏览器排队请求的时间。 黄色条形图是第一个字节的请求时间,即发送请求并从服务器接收第一个响应所需的时间。 蓝色条显示从服务器接收响应数据所需的时间。 可以双击资产以获取详细的计时信息。 例如,下图显示了加载 /Scripts/MyScripts/JavaScript6.js 文件的计时详细信息。
上图显示了 “开始” 事件,该事件提供由于浏览器限制同时连接数而排队请求的时间。 在这种情况下,请求已排队 46 毫秒,等待另一个请求完成。
捆绑
捆绑是 ASP.NET 4.5 中的一项新功能,可以轻松地将多个文件合并或捆绑到单个文件中。 可以创建 CSS、JavaScript 和其他捆绑包。 文件更少意味着 HTTP 请求更少,这可以提高第一页加载性能。
下图显示了前面显示的“关于”视图的相同计时视图,但这次启用了捆绑和缩小。
缩小
缩小对脚本或 css 执行各种不同的代码优化,例如删除不必要的空格和注释,并将变量名称缩短为一个字符。 请考虑以下 JavaScript 函数。
AddAltToImg = function (imageTagAndImageID, imageContext) {
///<signature>
///<summary> Adds an alt tab to the image
// </summary>
//<param name="imgElement" type="String">The image selector.</param>
//<param name="ContextForImage" type="String">The image context.</param>
///</signature>
var imageElement = $(imageTagAndImageID, imageContext);
imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}
缩小后,函数将减少到以下各项:
AddAltToImg = function (n, t) { var i = $(n, t); i.attr("alt", i.attr("id").replace(/ID/, "")) }
除了删除注释和不必要的空格外,还重命名了以下参数和变量名称,如下所示:
Original | 重 命名 |
---|---|
imageTagAndImageID | n |
imageContext | t |
imageElement | i |
捆绑和缩小的影响
下表显示了在示例程序中单独列出所有资产和使用捆绑和缩小(B/M)之间的几个重要差异。
使用 B/M | 没有 B/M | 更改 | |
---|---|---|---|
文件请求 | 9 | 34 | 256% |
已发送 KB | 3.26 | 11.92 | 266% |
接收的 KB | 388.51 | 530 | 36% |
加载时间 | 510 MS | 780 MS | 53% |
发送的字节随着浏览器在请求上应用的 HTTP 标头相当详细而大幅减少。 由于已缩小最大文件(Scripts\jquery-ui-1.8.11.min.js 和 Scripts\jquery-1.7.1.min.js),接收的字节数减少并不大。 注意:示例程序上的计时使用 Fiddler 工具模拟慢速网络。 (从 Fiddler “规则”菜单,选择“性能”,然后选择“模拟调制解调器速度”。
调试捆绑 JavaScript 和缩小 JavaScript
在开发环境中(其中 Web.config 文件中的编译元素设置为debug="true"
)中调试 JavaScript 很容易,因为 JavaScript 文件未捆绑或缩小。 还可以调试 JavaScript 文件捆绑和缩小的发布版本。 使用 IE F12 开发人员工具,使用以下方法调试缩小捆绑包中包含的 JavaScript 函数:
- 选择“ 脚本 ”选项卡,然后选择“ 开始调试 ”按钮。
- 选择包含想要使用资产按钮进行调试的 JavaScript 函数的捆绑包。
- 通过选择“配置”按钮,然后选择“格式化 JavaScript”来设置缩小的 JavaScript 的格式。
- 在 “搜索脚本 ”输入框中,选择要调试的函数的名称。 在下图中,在搜索脚本输入框中输入了 AddAltToImg。
有关使用 F12 开发人员工具进行调试的详细信息,请参阅 MSDN 文章 :使用 F12 开发人员工具调试 JavaScript 错误。
控制捆绑和缩小
通过在 Web.config 文件中的编译元素中设置调试属性的值来启用或禁用捆绑和缩小。 在以下 XML 中, debug
设置为 true,因此禁用捆绑和缩小。
<system.web>
<compilation debug="true" />
<!-- Lines removed for clarity. -->
</system.web>
若要启用捆绑和缩小,请将 debug
值设置为“false”。 可以使用类上的属性替代 Web.config 设置EnableOptimizations
。BundleTable
以下代码支持捆绑和缩小,并替代 Web.config 文件中的任何设置。
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
// Code removed for clarity.
BundleTable.EnableOptimizations = true;
}
注意
true
除非EnableOptimizations
将 Web.config 文件中编译元素中的调试属性设置为 false
,否则不会捆绑或缩小文件。 此外,不会使用 .min 版本的文件,将选择完整的调试版本。 EnableOptimizations
重写 Web.config 文件中编译元素中的调试属性
将捆绑和缩小与 ASP.NET Web 窗体和网页配合使用
将捆绑和缩小与 ASP.NET MVC 配合使用
在本部分中,我们将创建一个 ASP.NET MVC 项目来检查捆绑和缩小。 首先,创建一 个名为 MvcBM 的新 ASP.NET MVC Internet 项目,而无需更改任何默认值。
打开 App\_Start\BundleConfig.cs 文件并检查RegisterBundles
用于创建、注册和配置捆绑包的方法。 以下代码显示了方法的 RegisterBundles
一部分。
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
// Code removed for clarity.
}
前面的代码将创建一个名为 ~/bundles/jquery 的新 JavaScript 捆绑包,其中包含所有适当的(调试或缩小,但不是)。与通配符字符串“~/Scripts/jquery-{version}.js”匹配的 Scripts 文件夹中的 vsdoc 文件。 对于 ASP.NET MVC 4,这意味着使用调试配置,文件 jquery-1.7.1.js 将添加到捆绑包。 在发布配置中, 将添加jquery-1.7.1.min.js 。 捆绑框架遵循多种常见约定,例如:
- 选择“.min”文件,以便在存在FileX.min.js和FileX.js时发布。
- 选择要调试的非“.min”版本。
- 忽略仅由 IntelliSense 使用的“-vsdoc”文件(如 jquery-1.7.1-vsdoc.js)。
上面所示的{version}
通配符匹配用于在 Scripts 文件夹中自动创建包含相应版本的 jQuery 捆绑包。 在此示例中,使用通配符具有以下优势:
- 允许使用 NuGet 更新到较新的 jQuery 版本,而无需在视图页中更改前面的捆绑代码或 jQuery 引用。
- 自动为调试配置选择完整版本,并为发布版本选择“.min”版本。
使用 CDN
下面的代码将本地 jQuery 捆绑包替换为 CDN jQuery 捆绑包。
public static void RegisterBundles(BundleCollection bundles)
{
//bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
// "~/Scripts/jquery-{version}.js"));
bundles.UseCdn = true; //enable CDN support
//add link to jquery on the CDN
var jqueryCdnPath = "https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js";
bundles.Add(new ScriptBundle("~/bundles/jquery",
jqueryCdnPath).Include(
"~/Scripts/jquery-{version}.js"));
// Code removed for clarity.
}
在上面的代码中,jQuery 将在发布模式下从 CDN 请求,jQuery 的调试版本将在调试模式下本地提取。 使用 CDN 时,如果 CDN 请求失败,应具有回退机制。 布局文件末尾的以下标记片段显示添加到请求 jQuery 的脚本(如果 CDN 失败)。
</footer>
@Scripts.Render("~/bundles/jquery")
<script type="text/javascript">
if (typeof jQuery == 'undefined') {
var e = document.createElement('script');
e.src = '@Url.Content("~/Scripts/jquery-1.7.1.js")';
e.type = 'text/javascript';
document.getElementsByTagName("head")[0].appendChild(e);
}
</script>
@RenderSection("scripts", required: false)
</body>
</html>
创建捆绑包
Bundle 类Include
方法采用字符串数组,其中每个字符串都是资源的虚拟路径。 App\_Start\BundleConfig.cs 文件中方法的以下代码RegisterBundles
显示了如何将多个文件添加到捆绑包:
bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
"~/Content/themes/base/jquery.ui.core.css",
"~/Content/themes/base/jquery.ui.resizable.css",
"~/Content/themes/base/jquery.ui.selectable.css",
"~/Content/themes/base/jquery.ui.accordion.css",
"~/Content/themes/base/jquery.ui.autocomplete.css",
"~/Content/themes/base/jquery.ui.button.css",
"~/Content/themes/base/jquery.ui.dialog.css",
"~/Content/themes/base/jquery.ui.slider.css",
"~/Content/themes/base/jquery.ui.tabs.css",
"~/Content/themes/base/jquery.ui.datepicker.css",
"~/Content/themes/base/jquery.ui.progressbar.css",
"~/Content/themes/base/jquery.ui.theme.css"));
提供了 Bundle 类IncludeDirectory
方法以添加目录(可选)中与搜索模式匹配的所有子目录中的所有文件。 捆绑包类 IncludeDirectory
API 如下所示:
public Bundle IncludeDirectory(
string directoryVirtualPath, // The Virtual Path for the directory.
string searchPattern) // The search pattern.
public Bundle IncludeDirectory(
string directoryVirtualPath, // The Virtual Path for the directory.
string searchPattern, // The search pattern.
bool searchSubdirectories) // true to search subdirectories.
使用 Render 方法Styles.Render
(对于 CSS 和 Scripts.Render
JavaScript)在视图中引用捆绑包。 Views\Shared\_Layout.cshtml 文件中的以下标记显示了默认 ASP.NET Internet 项目视图引用 CSS 和 JavaScript 捆绑包的方式。
<!DOCTYPE html>
<html lang="en">
<head>
@* Markup removed for clarity.*@
@Styles.Render("~/Content/themes/base/css", "~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
@* Markup removed for clarity.*@
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</body>
</html>
请注意,Render 方法采用字符串数组,因此可以在一行代码中添加多个捆绑包。 通常,需要使用 Render 方法来创建引用资产所需的 HTML。 可以使用 Url
该方法生成资产的 URL,而无需引用资产所需的标记。 假设你想要使用新的 HTML5 异步 属性。 以下代码演示如何使用 Url
该方法引用新式化器。
<head>
@*Markup removed for clarity*@
<meta charset="utf-8" />
<title>@ViewBag.Title - MVC 4 B/M</title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/css")
@* @Scripts.Render("~/bundles/modernizr")*@
<script src='@Scripts.Url("~/bundles/modernizr")' async> </script>
</head>
使用“*”通配符选择文件
方法中指定的 Include
虚拟路径和方法中的 IncludeDirectory
搜索模式可以接受一个“*”通配符作为最后一个路径段的前缀或后缀。 搜索字符串不区分大小写。 该方法 IncludeDirectory
可以选择搜索子目录。
请考虑具有以下 JavaScript 文件的项目:
- Scripts\Common\AddAltToImg.js
- Scripts\Common\ToggleDiv.js
- Scripts\Common\ToggleImg.js
- Scripts\Common\Sub1\ToggleLinks.js
下表显示了使用通配符添加到捆绑包的文件,如下所示:
调用 | 已添加文件或引发异常 |
---|---|
Include(“~/Scripts/Common/*.js”) | AddAltToImg.js、 ToggleDiv.js、 ToggleImg.js |
Include(“~/Scripts/Common/T*.js”) | 模式异常无效。 仅允许在前缀或后缀上使用通配符。 |
Include(“~/Scripts/Common/*og.*”) | 模式异常无效。 仅允许一个通配符。 |
Include(“~/Scripts/Common/T*”) | ToggleDiv.js、ToggleImg.js |
Include(“~/Scripts/Common/*”) | 模式异常无效。 纯通配符段无效。 |
IncludeDirectory(“~/Scripts/Common”, “T*”) | ToggleDiv.js、ToggleImg.js |
IncludeDirectory(“~/Scripts/Common”, “T*”, true) | ToggleDiv.js、 ToggleImg.js、 ToggleLinks.js |
将每个文件显式添加到捆绑包通常首选于加载文件的通配符,原因如下:
通过通配符添加脚本默认按字母顺序加载这些脚本,这通常不是所需的。 CSS 和 JavaScript 文件通常需要按特定(非字母顺序)添加。 可以通过添加自定义 IBundleOrderer 实现来缓解此风险,但显式添加每个文件不太容易出错。 例如,将来可能会向文件夹添加新资产,这可能需要修改 IBundleOrderer 实现。
使用通配符加载添加到目录的特定文件可包含在引用该捆绑包的所有视图中。 如果将特定于视图的脚本添加到捆绑包,则可能会在引用捆绑包的其他视图上收到 JavaScript 错误。
导入其他文件的 CSS 文件会导致导入的文件加载两次。 例如,以下代码创建一个捆绑包,其中大多数 jQuery UI 主题 CSS 文件加载了两次。
bundles.Add(new StyleBundle("~/jQueryUI/themes/baseAll") .IncludeDirectory("~/Content/themes/base", "*.css"));
通配符选择器“*.css”将引入文件夹中的每个 CSS 文件,包括 Content\themes\base\jquery.ui.all.css 文件。 jquery.ui.all.css文件导入其他 CSS 文件。
捆绑缓存
捆绑包从创建捆绑包起的一年内设置 HTTP 过期标头。 如果导航到以前查看的页面,Fiddler 会显示 IE 不对捆绑包发出条件请求,即没有来自 IE 的 HTTP GET 请求,也没有来自服务器的 HTTP 304 响应。 可以强制 IE 为每个捆绑包发出条件请求,其中包含 F5 密钥(导致每个捆绑包的 HTTP 304 响应)。 可以使用 ^F5 强制完全刷新(导致每个捆绑包的 HTTP 200 响应)。
下图显示了 Fiddler 响应窗格的“缓存 ”选项卡:
请求
http://localhost/MvcBM_time/bundles/AllMyScripts?v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81
用于捆绑 AllMyScripts ,包含查询字符串对 v=r0sLDicvP58AIXN\_mc3QdyVvVj5euZNzdsa2N1PKvb81。 查询字符串 v 具有一个值令牌,该令牌是用于缓存的唯一标识符。 只要捆绑包未更改,ASP.NET 应用程序就会使用此令牌请求 AllMyScripts 捆绑包。 如果捆绑包中的任何文件发生更改,ASP.NET 优化框架将生成一个新令牌,保证对捆绑包的浏览器请求将获取最新的捆绑包。
如果运行 IE9 F12 开发人员工具并导航到以前加载的页面,IE 错误地显示对每个捆绑包发出的条件 GET 请求,以及返回 HTTP 304 的服务器。 你可以阅读为什么 IE9 在博客文章 中使用 CDN 和 Expires 改善网站性能时遇到条件请求时出现问题。
LESS、CoffeeScript、SCSS、Sass 捆绑。
捆绑和缩小框架提供了一种机制,用于处理中间语言(如 SCSS、Sass、LESS 或 Coffeescript),并将转换(如缩小)应用于生成的捆绑包。 例如,若要将 .less 文件添加到 MVC 4 项目:
为 LESS 内容创建文件夹。 以下示例使用 Content\MyLess 文件夹。
将 .less NuGet 包 无点 添加到项目中。
添加实现 IBundleTransform 接口的类。 对于 .less 转换,请将以下代码添加到项目。
using System.Web.Optimization; public class LessTransform : IBundleTransform { public void Process(BundleContext context, BundleResponse response) { response.Content = dotless.Core.Less.Parse(response.Content); response.ContentType = "text/css"; } }
使用
LessTransform
CssMinify 转换创建 LESS 文件的捆绑包。 将以下代码添加到RegisterBundles
App\_Start\BundleConfig.cs 文件中的方法。var lessBundle = new Bundle("~/My/Less").IncludeDirectory("~/My", "*.less"); lessBundle.Transforms.Add(new LessTransform()); lessBundle.Transforms.Add(new CssMinify()); bundles.Add(lessBundle);
将以下代码添加到引用 LESS 捆绑包的任何视图。
@Styles.Render("~/My/Less");
捆绑包注意事项
创建捆绑包时要遵循的一个很好的约定是将“bundle”作为前缀包含在捆绑名称中。 这将阻止可能的 路由冲突。
在捆绑包中更新一个文件后,将为捆绑查询字符串参数生成一个新令牌,并且下次客户端请求包含捆绑包的页面时必须下载完整捆绑包。 在单独列出每个资产的传统标记中,仅下载已更改的文件。 经常变化的资产可能不是捆绑的好候选项。
捆绑和缩小主要缩短第一个页面请求加载时间。 请求网页后,浏览器将缓存资产(JavaScript、CSS 和图像),因此在请求同一页面或请求相同资产的同一站点上的页面时,捆绑和缩小不会提供任何性能提升。 如果未在资产上正确设置过期标头,并且未使用捆绑和缩小,浏览器新鲜度启发式会在几天后标记资产过期,并且浏览器需要对每个资产进行验证请求。 在这种情况下,捆绑和缩小在第一页请求后提供性能提升。 有关详细信息,请参阅博客 使用 CDN 和过期以提高网站性能。
使用 CDN 可以缓解每个主机名的 6 个同时连接的浏览器限制。 由于 CDN 的主机名与托管站点不同,因此来自 CDN 的资产请求不会计入与托管环境的六个同时连接限制。 CDN 还可以提供常见的包缓存和边缘缓存优势。
捆绑包应按需要它们的页面进行分区。 例如,Internet 应用程序的默认 ASP.NET MVC 模板创建独立于 jQuery 的 jQuery 验证捆绑包。 由于创建的默认视图没有输入且不发布值,因此它们不包括验证捆绑包。
命名空间System.Web.Optimization
在 System.Web.Optimization.dll 中实现。 它利用 WebGrease 库(WebGrease.dll)实现缩小功能,后者又使用 Antlr3.Runtime.dll。
我使用 Twitter 制作快速帖子和共享链接。 我的 Twitter 句柄是: @RickAndMSFT
其他资源
- 视频:霍华德·迪尔金的捆绑和优化
- 将 Web 优化添加到网页网站。
- 将捆绑和缩小添加到 Web 窗体。
- Henrik F Nielsen 对 Web 浏览进行捆绑和缩小的性能影响@frystyk
- 使用 CDN 和 Expires 提高 Rick Anderson @RickAndMSFT的网站性能
- 最小化 RTT (往返时间)
供稿人
- 浩功
- 霍华德·迪尔金
- 戴安娜·拉罗斯