ASP.NET MVC ETag & Cache and other optimization methods

background

A recent project is to use SmartAdmin + Jquery + EasyUI to create an ASP Net mvc5 project, there has always been a performance problem. The loading speed is relatively slow. The first loading (without cache) takes 4-5 seconds to complete all loading

The following figure shows the test results with Chrome PageSpeed

There are several very important indicators

First contentfu paint: it takes 4 seconds to draw the page for the first time. The first 4 seconds are white, which is really a little long

First meaningfull paint: it takes 8.6 seconds to draw meaningful content for the first time before the visible operation page appears

Elite render blocking resources: blocking the loading of resource files because jquery and css are loaded in the head of your project because some code must be executed first

Remove unused css: there are a lot of useless css style definitions, which is also difficult to avoid

Analyze the reason

The main reason for the above problems is that the size of the page itself and all resources add up to more than 3.2m. JQuery easyUI's JS+css is close to 3M. In addition, several partialviews are embedded in the page, and the time to execute js. EasyUI DataGrid needs to grab data from the background and generate complex Dom structure, which takes time

General optimization means

Cache

The first thing I think of is to use cache. It can only solve the speed problem of the second access. It is rarely used. I usually do this. The setting methods are

  • Add outputcache, for example:
[OutputCache(Duration = 360, VaryByParam = "none")]
public ActionResult Index() => this.View();
  • web.Config add cache for static files
<system.webServer>
        <staticContent>
            <remove fileExtension=".js" />
            <mimeMap fileExtension=".js" mimeType="text/javascript" />
            <remove fileExtension=".ico" />
            <mimeMap fileExtension=".ico" mimeType="image/x-icon" />
            <remove fileExtension=".eot" />
            <mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
            <remove fileExtension=".woff" />
            <mimeMap fileExtension=".woff" mimeType="application/x-font-woff" />
            <remove fileExtension=".woff2" />
            <mimeMap fileExtension=".woff2" mimeType="application/x-font-woff2" />
            <remove fileExtension=".svg" />
            <mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
            <remove fileExtension=".ttf" />
            <mimeMap fileExtension=".ttf" mimeType="application/x-font-ttf" />
            <clientCache cacheControlMode="UseMaxAge" httpExpires="365.00:00:00" cacheControlMaxAge="365.00:00:00" />
        </staticContent>
</system.webServer>

Compress and merge resource files

Minimize the size of the resource file and the number of requests. The usual approach is to use bundleconfig CS merges and compresses JS and CSS files I now use bundleconfig JSON configuration instead of system Web. Optimization. The configuration should be flexible. If you use bundleconfig JSON compilation compression also needs to solve the problem of client-side update cache. I use the following code to add a fingerprint flag

public class Fingerprint
    {
        public static string Tag(string rootRelativePath)
        {
            if (HttpRuntime.Cache[rootRelativePath] == null)
            {
                string absolute = HostingEnvironment.MapPath("~" + rootRelativePath);

                DateTime date = File.GetLastWriteTime(absolute);
                int index = rootRelativePath.LastIndexOf('/');

                string result = rootRelativePath.Insert(index, "/v-" + date.Ticks);
                HttpRuntime.Cache.Insert(rootRelativePath, result, new CacheDependency(absolute));
            }

            return HttpRuntime.Cache[rootRelativePath] as string;
        }
    }
<system.webServer>
   <urlCompression doStaticCompression="true" doDynamicCompression="true" dynamicCompressionBeforeCache="false" />
   <rewrite>
      <rules>
        <rule name="fingerprint">
          <match url="([\S]+)(/v-[0-9]+/)([\S]+)" />
          <action type="Rewrite" url="{R:1}/{R:3}" />
        </rule>
      </rules>
    </rewrite>
 </system.webServer>
<link rel="stylesheet" href="@Fingerprint.Tag("/content/site.css")" />

ETag

ETags is a tool for Web cache validation that allows conditional client requests. Through ETags, the browser can judge whether a resource is needed. If not, the browser will not send requests to the web server, minimizing the number of requests. Configuration method

  • Global scheme, customize an HttpModule
  public class ETagHttpModule : IHttpModule
  {
    #region IHttpModule Members
    void IHttpModule.Dispose()
    {
      // Nothing to dispose; 
    }
    void IHttpModule.Init(HttpApplication context)
    {
      context.BeginRequest += new EventHandler(context_BeginRequest);
      WebPageHttpHandler.DisableWebPagesResponseHeader = true;
    }
    #endregion
    void context_BeginRequest(object sender, EventArgs e)
    {
      HttpApplication app = sender as HttpApplication;

      //if (app.Request.CurrentExecutionFilePath.EndsWith("/") || app.Request.CurrentExecutionFilePath.EndsWith(".cshtml"))
      //{
        app.Response.Filter = new ETagStream(app.Response, app.Request);
      //}
    }
    #region Stream filter
    public class ETagStream : MemoryStream
    {
      private HttpResponse _response = null;
      private HttpRequest _request;
      private Stream _filter = null;
      public ETagStream(HttpResponse response, HttpRequest request)
      {
        _response = response;
        _request = request;
        _filter = response.Filter;
      }
      private string GetToken(Stream stream)
      {
        var checksum = new byte[0];
        checksum = MD5.Create().ComputeHash(stream);
        return Convert.ToBase64String(checksum, 0, checksum.Length);
      }
      public override void Write(byte[] buffer, int offset, int count)
      {
        var data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        var token = GetToken(new MemoryStream(data));
        var clientToken = _request.Headers["If-None-Match"];
        if (token != clientToken)
        {
          _response.AddHeader("ETag", token);
          _filter.Write(data, 0, count);
        }
        else
        {
          _response.SuppressContent = true;
          _response.StatusCode = 304;
          _response.StatusDescription = "Not Modified";
          _response.AddHeader("Content-Length", "0");
        }
      }
    }
    #endregion
  }
<modules>
            <remove name="FormsAuthentication" />
            <!--<add type="WhitespaceModule" name="WhitespaceModule" />-->
           <add type="WebApp.ETagHttpModule" name="ETagHttpModule" />
</modules>
  • Action page level
public class ETagAttribute : ActionFilterAttribute
  {
    public override void OnActionExecuting(ActionExecutingContext filterContext) => filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request);
  }

  public class ETagFilter : MemoryStream
  {
    private HttpResponseBase _response = null;
    private HttpRequestBase _request;
    private Stream _filter = null;

    public ETagFilter(HttpResponseBase response, HttpRequestBase request)
    {
      _response = response;
      _request = request;
      _filter = response.Filter;
    }

    private string GetToken(Stream stream)
    {
      var checksum = new byte[0];
      checksum = MD5.Create().ComputeHash(stream);
      return Convert.ToBase64String(checksum, 0, checksum.Length);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
      var data = new byte[count];

      Buffer.BlockCopy(buffer, offset, data, 0, count);

      var token = GetToken(new MemoryStream(data));
      var clientToken = _request.Headers["If-None-Match"];

      if (token != clientToken)
      {
        _response.AddHeader("ETag", token);
        _filter.Write(data, 0, count);
      }
      else
      {
        _response.SuppressContent = true;
        _response.StatusCode = 304;
        _response.StatusDescription = "Not Modified";
        _response.AddHeader("Content-Length", "0");
      }
    }

   
  }
//[OutputCache(Duration = 360, VaryByParam = "none")]
[ETag]
public ActionResult Index() => this.View();

In the rendering, the number of bytes sent back has been greatly reduced, and the single response time is almost the same, which is not very obvious

summary

There are many optimization schemes, but the effect is not ideal. To achieve the ultimate user experience, you may really have to abandon Jquery,EasyUI and other large and complex class libraries

problem

In addition, do you have a very easy and simple way to solve the problem of initial loading white screen? I tried to use js preloading layer animation, but the effect is still not ideal But I've seen some websites and apps, and the results are very good. I don't know how to implement them. In ASP Net MVC environment can be used

Reference articles

(ten ways to speed up ASP.NET MVC applications) http://blog.oneapm.com/apm-tech/679.html

Added by msore on Sat, 25 Dec 2021 17:59:39 +0200