I recently got to work on a rapid re-brand of the UI for an existing Asp.Net MVC 4.6 web application. I kept a close eye on the Lighthouse tool from Google. It was my goal to get us the best score we could with the limited time frame. After that experience, I think this tool should be run constantly by dev teams and possible even integrated into a build.
I also wanted to improve the security of our site. I found a few tools, along with Lighthouse, that pointed out easy changes to make. The Asaf Web tool from Troy Hunt pointed out headers and other things that improve web site security. The Webhint scanner from JS Foundation “is a linting tool” and points out even more security, performance, PWA, accessibility and other improvements that can be made.
Read more about Webhint which should also be ran often or integrated into your build.
Note: Sonarwhal is now “WebHint.io .
Troy Hunt tells us more about which headers not to serve and has a lot of great talks and resources about security.
Even though I was working on a pre-Core site, I have a short section for Asp.Net Core 2.1+ at the bottom. I’m working on converting this same site to Core, but haven’t had a lot of time available, so it is slow going.
I started by running Lighthouse (extension for Chrome) locally and Webhint against my dev site. There were many suggestions from both. Webhint uses multiple tools, has great documentation for each suggestion, and gives you a perma-link to share with others or go back to to see your improvements.
I have several things in this section alone.
Clicking more details shows each individual warning.
Some of the errors are due to using CDNs. Shouldn’t they be checking their site with this tool too? I can either ignore these and be ok with an imperfect score or move copies to my server. The worst seem to be Google Ads :-).
We can do this by removing the following headers.
x-aspnet-version: 4.0.30319
x-aspnetmvc-version: 5.2
x-powered-by: ASP.NET
server: Microsoft-IIS/10.0
Here’s an excerpt from the web.config below to remove those headers.
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By"/>
<remove name="X-AspNetMvc-Version"/>
<remove name="server"/>
.... more ....
I have more in the web.config and more work I need to do on my site, but the tools pointed out things I hadn’t thought about before and gave helpful instruction on avoiding these potential problems.
The web.config instructs IIS, therefore, it should work for Asp.Net Core sites that use IIS.
I
requested a base web.config from Webhint
to make this whole process easier, but when someone just gives us something, we won’t learn as much :-). Maybe dotnet new mvc
and Visual Studio File > New Project could just include these by default someday!
Update: 8/9/2018 There’s a Pull Request linked to my issue. It’s a great place to start and nice to see it will be directly in the documentation. I have some good information, but I would use Molant ’s work as the main source of a web.config.
We can improve our Lighthouse and Webhint scores by enabling HTTP2, optimizing image scores and setting the correct caching rules.
Mad Kristensen offered some great pointers for Asp.Net and IIS . I’ve added/duplicated some here.
We’re on Azure AppServices, and they’ve made it really easy to turn on Http2 . Here’s more info on Http2 and how it helps. I highly recommend turning this on, if you have the option. It increased my score and performance scores. As with all of the recommendations, Lighthouse has a helpful link .
Here’s the Lighthouse recommendation screenshot:
Quite costing your customers bandwidth and making your site slow. It’s easy to optimize images with TinyPng through the website or with their API and Node. You can go further and using better image formats, correctly sizing images and defer off-screen images. SVGs are another great way to reduce image sizes.
You don’t want your users to have to download the same images, javascript and CSS every time. That’s where HTTP caching comes in. The browser will ask if something is new and the server returns a 304. To see this in action, open Dev tools in Chrome (F12) with the network tab open and check disable cache. Load the site. Then uncheck disable cache. Notice the difference in bytes downloaded and all the 304’s.
Here’s a snippet from my web.config below. You need one of these for each folder.
Be aware that in this example, for the user to get a new image, we’d have to rename the file name. For our JavaScript and CSS we are using a GulpJs script to combine, minimize and inject a file that has a unique hash in it to bust the cache. We could probably do the same thing for image file names, but haven’t yet.
SO question was helpful, as usual
<!-- 60480 hours = 30 days * 12 -->
<location path="Images">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
Unfortunately in the web.config, this has to be duplicated for every sub folder as well
Here’s our js folder
<location path="dist/js">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
The service worker is a key component to PWAs and will increase performance and your Lighthouse score. Checkout my many links on PWA that I keep adding to.
I highly suggest the Syntax.fm podcast on “20 Easy Win Performance Tips” for more ideas to improve your website performance.
You can spend a lot of time and learn a lot trying to get your score higher. Be sure to check out Google’s web resources that are also linked from the Lighthouse reports.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<connectionStrings>
<add name="MyDBContext" connectionString="Initial Catalog=MyDbB;Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.net>
<mailSettings>
</mailSettings>
</system.net>
<appSettings>
<add key="appInsightsKey" value="" />
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
<system.web>
<authentication mode="Forms" />
<compilation debug="true" targetFramework="4.6.2" />
<customErrors mode="Off">
<error statusCode="404" redirect="~/Error/NotFound" />
<error statusCode="500" redirect="~/Error/ServerError" />
</customErrors>
<httpRuntime targetFramework="4.6.2" maxRequestLength="1048576" enableVersionHeader="false" relaxedUrlToFileSystemMapping="true" requestPathInvalidCharacters="" requestValidationMode="4.0" />
<httpModules>
<add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
</httpModules>
<!-- https://www.troyhunt.com/c-is-for-cookie-h-is-for-hacker/ -->
<httpCookies httpOnlyCookies="true" requireSSL="true" />
</system.web>
<system.webServer>
<modules>
<remove name="FormsAuthentication" />
<remove name="TelemetryCorrelationHttpModule" />
<add name="TelemetryCorrelationHttpModule" type="Microsoft.AspNet.TelemetryCorrelation.TelemetryCorrelationHttpModule, Microsoft.AspNet.TelemetryCorrelation" preCondition="integratedMode,managedHandler" />
<remove name="ApplicationInsightsWebTracking" />
<add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" />
</modules>
<httpProtocol>
<!-- https://serverfault.com/questions/24885/how-to-remove-iis-asp-net-response-headers -->
<customHeaders>
<remove name="Strict-Transport-Security" />
<add name="Strict-Transport-Security" value="max-age=15768000"/>
<remove name="X-Frame-Options" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<remove name="X-XSS-Protection" />
<add name="X-XSS-Protection" value="1; mode=block"/>
<remove name="X-Content-Type-Options"/>
<add name="X-Content-Type-Options" value="nosniff"/>
<remove name="X-Powered-By"/>
<remove name="X-AspNetMvc-Version"/>
<remove name="server"/>
<!--<remove name="Content-Security-Policy" />
<add name="Content-Security-Policy" value="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' https://www.google-analytics.com; script-src 'self' https://code.jquery.com/; script-src 'self' https://ajax.googleapis.com/ script-src 'self' https://cdn.jsdelivr.net/ " />-->
<remove name="P3P" />
<add name="P3P" value="policyref="/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"" />
</customHeaders>
</httpProtocol>
<security>
<requestFiltering removeServerHeader="true" />
</security>
<staticContent>
<!-- ==================== ADDED FOR ISSUE WITH glyphicons-halflings-regular.woff2 404 error on live server ==============-->
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
<!-- ==================== ADDED to serve the manifest.json on AppService server ==============-->
<remove fileExtension=".json" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<!--=====================================================================================================================-->
</staticContent>
<!-- examples from https://github.com/h5bp/server-configs-iis/blob/master/dotnet%204/mvc4%20%26%20mvc4api/web.config-->
<directoryBrowse enabled="false"/>
<validation validateIntegratedModeConfiguration="false" />
<httpCompression directory="%SystemDrive%\websites\_compressed" minFileSizeForComp="1024">
<scheme dll="%Windir%\system32\inetsrv\gzip.dll" name="gzip"/>
<staticTypes>
<add enabled="true" mimeType="text/*"/>
<add enabled="true" mimeType="message/*"/>
<add enabled="true" mimeType="application/javascript"/>
<add enabled="true" mimeType="application/json"/>
<add enabled="false" mimeType="*/*"/>
</staticTypes>
</httpCompression>
<urlCompression doDynamicCompression="true" doStaticCompression="true" dynamicCompressionBeforeCache="true" />
</system.webServer>
<!-- https://stackoverflow.com/questions/2195266/how-to-configure-static-content-cache-per-folder-and-extension-in-iis7
60480 hours = 30 days * 12 -->
<location path="Images">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<location path="Images/LandingPage">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<location path="Images/DefaultProfilePics">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<location path="Images/StaticAds">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<location path="dist/css">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<location path="dist/js">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<location path="dist/svg">
<system.webServer>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="60480.00:00:00" />
</staticContent>
</system.webServer>
</location>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="mssqllocaldb" />
</parameters>
</defaultConnectionFactory>
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
</entityFramework>
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.8.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.8.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\"Web\" /optionInfer+" />
</compilers>
</system.codedom>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- redirects here -->
</assemblyBinding>
</runtime>
</configuration>
Use NWebSec Middleware to improve security instead of the web.config. I think I wil try this out next time and add some notes here in the future.
Here’s the official documentation for NWebSec
Go run Webhint and Lighthouse on your sites! Then start making incremental improvements to make the web more secure and faster.
Please consider using Brave and adding me to your BAT payment ledger. Then you won't have to see ads! (when I get to $100 in Google Ads for a payout, I pledge to turn off ads)
Also check out my Resources Page for referrals that would help me.