Visual Studio Technical Articles
Performance Tips and Tricks for ASP.NET Pages Using JScript .NET
 

Visual Studio Team
Microsoft Corporation

January 2002

Summary: This article helps you improve the performance of ASP.NET pages written in JScript® .NET, particularly pages upgraded from ASP pages written in JScript. The emphasis is on the new features of ASP.NET and JScript .NET that are most useful for enhancing performance. (15 printed pages)

Requires:

  • Some experience with ASP.NET and JScript .NET
  • One of the following platforms: Windows® 2000 (Professional, Server, or Advanced Server) with Service Pack 2.0 or Windows® XP Professional
  • Microsoft® Internet Information Services (IIS) 5.0
  • The .NET Framework
    Note   Both Visual Studio® .NET and the .NET Framework SDK automatically install the .NET Framework. For more information, see Five Steps to Getting Started with ASP.NET.

Contents

Introduction
How to Overcome COM Interop Bottlenecks
How to Use Type Annotation
How to Use JScript .NET Efficiently
Conclusions

Introduction

An ASP.NET page can run significantly faster than an equivalent ASP page because an ASP.NET page runs as compiled code. Conversely, the Web server must interpret each ASP page. However, simply upgrading a page from ASP to ASP.NET does not guarantee improved performance.

This article discusses some simple techniques that leverage the flexibility of the .NET Framework for improving the performance of ASP.NET pages. First, the paper discusses improvements for pages that use COM components. Second, the paper discusses some new performance-enhancing features of the JScript .NET language. Finally, there are general tips for writing efficient JScript .NET code. Most of the discussed changes are easy to make and improve the flow, reliability, readability, and performance of the code.

How to Overcome COM Interop Bottlenecks

Code in an ASP.NET page does not call COM components directly. Instead, the .NET Framework creates a runtime callable wrapper (RCW) that serves as a proxy between the managed code in the ASP.NET page and the unmanaged code in the COM component. For more information, see Microsoft .NET/COM Migration and Interoperability. Because of the overhead from the wrapper converting every piece of data that passes through it, an ASP.NET page that uses COM objects is likely to have poorer performance than an equivalent ASP page.

To improve the performance of an ASP.NET page that uses COM components, you can use an interop assembly, essentially an optimized RCW, for each COM component. You can also use an equivalent .NET component instead of the COM component. The first approach is easier to do, while the second approach provides better performance.

Using Interop Assemblies

Although the RCW allows ASP.NET pages to easily access COM components, it is slow. One way to improve performance is to use an interop assembly instead of the default RCW. An interop assembly contains metadata that represents the type library for the COM component. The interop assembly is a new DLL that managed clients (such as ASP.NET) can use to access the COM component as if it were a .NET class. It uses .NET Framework data types to early-bind the parameters of the COM component methods. This substantially reduces the per-call overhead for the methods, although some data conversion still occurs.

You should use the primary interop assembly for the COM component, if one is available. A primary interop assembly is an interop assembly that the publisher has digitally signed.

Note   Installing Visual Studio .NET registers and installs primary interop assemblies for several commonly used COM components, such as ADO and MSHTML. The default location is: C:\Program Files\Microsoft.NET\Primary Interop Assemblies.

If no primary interop assembly is available for your COM component, you can create a new interop assembly with the Type Library Importer tool, Tlbimp.exe. This tool is included with the .NET Framework SDK and is run from the command prompt. It creates an interop assembly based on the type information in the type library for the COM component. By default, the name of the interop assembly and the COM component namespace is the same.

Before you can use Tlbimp.exe to create an interop assembly, you must first find the location of the COM component.

To locate the CLSID for a COM component

  1. From a command prompt, run RegEdit.exe.
    Caution   RegEdit.exe allows you to view and modify the registry settings for your machine. Be careful not change registry settings.
  2. Expand the My Computer tree and then expand the HKEY_CLASSES_ROOT hive.
  3. Expand the key that has the same name as the COM component. For example, if you are searching for the ADODB COM component, you can expand the ADODB.Connection key.
  4. Click the CLSID subkey.

    Values appear in the right pane.

  5. Double-click the (Default) value in the right pane.

    The Edit String window opens.

  6. In the Edit String window, copy the CLSID value from the Value data field and then click Cancel.

    For example, a typical CLSID will have the form {00000514-0000-0010-8000-00AA006D2EA4}.

    Caution   Be careful not to make changes or click OK by mistake.

    Keep the Registry Editor open.

Now that you know the CLSID, you can use RegEdit.exe to find the COM component library that is associated with the CLSID.

To locate the COM library from a CLSID value

  1. In the Registry Editor, select My Computer.
  2. On the Edit menu, click Find and paste the CLSID into the Find what dialog box.

    Make sure that only the Keys check box is selected.

  3. Click the Find Next button to locate keys identified by the CLSID and expand the found key.

    The key includes one of the following subkeys: InProcServer32 , LocalServer32 or TypeLib. If the key includes the InProcServer32 subkey or the LocalServer32 subkey, go to the next step. Otherwise, go to the procedure labeled "To locate the COM library from a TypeLib value".

  4. Click the InProcServer32 subkey or the LocalServer32 subkey.

    Values appear in the right pane.

  5. Double-click the (Default) value to open the Edit String window.
  6. In the Edit String window, copy the fully qualified name for the DLL from the Value data field and then click Cancel.

    For example, the name may be "C:\Program Files\Common Files\system\ado\msado15.dll".

    Caution   Be careful not to make changes or click OK by mistake.

Now that you have a copy of the name of the DLL, go on to the procedure labeled "To create a new interop assembly".

To locate the COM library from a TypeLib value

  1. Click TypeLib subkey.

    Values appear in the right pane.

  2. Double-click the (Default) value to open the Edit String window.
  3. In the Edit String window, copy the TypeLib value from the Value data field and then click Cancel.
    Caution   Be careful not to make changes or click OK by mistake.
  4. Select My Computer in the left pane to search the entire registry for the TypeLib value.
  5. On the Edit menu, click Find and paste the TypeLib value into the Find what dialog box.

    Make sure that only the Keys check box is selected.

  6. Click the Find Next button to locate the key associated with the TypeLib value and expand the found key.

    It contains one or more subkeys identified with version numbers, such as 1.0.

  7. Expand any version number subkey.

    This subkey contains subkeys for each installed language; 0 is the default language.

  8. Expand any numbered subkey.
  9. Click the win32 subkey.

    Values appear in the right pane.

  10. Double-click the (Default) value to open the Edit String window.
  11. In the Edit String window, copy the fully qualified name for the DLL from the Value data field and then click Cancel.

    For example, the name may be "C:\Program Files\Microsoft SQL Server\80\COM\sqlmergx.dll".

    Caution   Be careful not to make changes or click OK by mistake.

After you have found the fully qualified name of the COM library, either in step 6 of the procedure labeled "To locate the COM library from a CLSID value", or in step 11 of the procedure labeled "To locate the COM library from a TypeLib value", it is easy to create the interop assembly.

To create a new interop assembly

  1. Open a command prompt and change to the root directory by typing:
    cd c:\
  2. Pass the fully qualified name of the COM library to Tlbimp.exe to produce the interop assembly. This creates an interop assembly with the same name as the COM component (which may be different from the name of the COM library) in the root directory.

    You may need call Tlbimp.exe using the fully qualified name. It is installed by default at C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin. For example, type the following command as one continuous string to produce the ADODB.dll interop assembly.

    "C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin\tlbimp.exe"
    "C:\Program Files\Common Files\System\ADO\msado15.dll"
    Note   Tlbimp.exe generates a warning if you create an interop assembly for a COM component when a primary interop assembly is already registered. If this happens, delete the newly created interop assembly and proceed to the procedure labeled "To reference the interop assembly".
  3. Move the interop assembly into the appropriate directory.

    If your application does not use a global.asax file, move the interop assembly from C:\ to C:\Inetpub\wwwroot\bin, the Web site's root directory. Otherwise, move the interop assembly to the bin subdirectory of the directory that contains the application's global.asax file.

    Note   You do not need to copy primary interop assemblies to the bin directory; they are registered in the global assembly cache (GAC).

After you have access to an interop assembly for your COM component, either from a primary interop assembly or from a newly created interop assembly, you can modify your ASP.NET page to access the assembly.

To reference the interop assembly

  1. To reference the interop assembly instead of the COM object, use the @Import and @Assembly directives at the start of the ASP.NET page after the <%@ language=JScript %> directive.

    For example, to reference the ADODB interop assembly, add the following:

    <%@ Import Namespace="ADODB" %>
    <%@ Assembly Name="ADODB" %>
  2. In each <object> tag that references the COM component, change the progid attribute to class.

    For example, you may have an <object> tag similar to the following:

    <object runat=server id="appConn" progid="ADODB.Connection" scope="Application" >
    </object>

    To access the interop assembly, use:

    <object runat=server id="appConn" class="ADODB.Connection" scope="Application" >
    </object>
  3. Change the lines in which the ActiveXObject object constructor or the Server.CreateObject method creates an instance of the COM component.

    The interop assembly allows you to call the constructor for the COM component directly and use type annotation. For example, you may have a call to the ActiveXObject object constructor similar to the following:

    var rs = new ActiveXObject("ADODB.Recordset");

    To use type annotation for the variable and to call the COM constructor directly, use the following syntax:

    var rs : RecordSet = new Recordset;

The ASPCOMPAT Attribute

Some COM components, such as those developed using Microsoft® Visual Basic® 6.0, are single-threaded apartment (STA) COM components. If you reference an STA component in an ASP.NET page without specifying that the page should run in compatibility mode, ASP.NET generates an error. To correct the error, include the aspcompat compatibility attribute in the <%@ Page > tag:

<%@ Page aspcompat=true Language = JScript %>
Note   Although ASP.NET cannot determine whether a component uses the STA model when you use the component's interop assembly, you must still use the aspcompat compatibility attribute. Otherwise, the application can suffer from poor performance and possible deadlocks.

Using Equivalent .NET Components

As you may suspect from the previous section, you should use .NET components instead of COM components for optimal performance in ASP.NET pages. All .NET components are written in managed code and are called directly without requiring a wrapper.

Many common COM components have equivalent .NET components that deliver similar functionality, although there are differences in the exact syntax for calling the methods. Consult the .NET Framework documentation for detailed information on calling .NET components.

The following table lists several equivalent components. Consult the .NET Framework documentation for .NET components that are equivalent to other COM components.

Note   Namespaces and classes are the .NET Framework's equivalent to components and component objects, respectively.

If you own the COM component have access to the source code, you can upgrade the component to managed code and create your own .NET component. There are many benefits to upgrading your components: strongly typed variables, increased security, access to the .NET Framework, and so on. For more information, see Microsoft .NET/COM Migration and Interoperability.

How to Use Type Annotation

JScript .NET includes many useful features that are not present in previous versions. Chief among these is the ability to type-annotate variables, which creates early-bound code. While this addition makes JScript .NET a strongly typed language, it still supports untyped (also known as loosely-typed or late-bound) variables. Early binding optimizes performance because it enables the compiler to generate specialized code for a specified type. It also enables the compiler to detect many common errors.

Type Annotation of Variables and Parameters

You should use early binding for all variables and function arguments. Because ASP.NET compiles JScript .NET code in fast mode, you must explicitly define all variables. It is easy to specify a data type for the variable at the same time. It is equally easy to use early binding for function parameters and return values.

Specifying data types for variables helps enforce good programming habits and helps eliminate errors such as accidentally storing the wrong data in a variable. For example, if you have a loop variable that you know stores only integer values, you can specify this explicitly. You use a colon to specify the variable type.

for( var i : int = 0; i<10; i++)
   x += i;

You can use any data type that JScript .NET provides to type-annotate your variables. In addition, you can declare new data types (classes) and use them for annotation. For more information, see JScript Data Types.

Use .NET Equivalents of JScript Objects

JScript .NET provides several prototype-based objects, called JScript objects, which you can use for type annotation. However, JScript objects are fundamentally different from the class-based objects that the .NET Framework provides. Many of these objects interoperate with the corresponding .NET Framework data type. In other words, JScript implicitly converts data from the JScript object to and from the .NET Framework data type as needed. For example, the Date object interoperates with the System.DateTime class. This means that a Date object can be used in contexts in which an instance of the System.DateTime class is expected, and vice versa.

Because conversions take time, you should type-annotate each variable to match the context in which you use the variable. For example, if you use the JScript .NET functions to manipulate dates and times, you should type-annotate your variable as a Date object. If you use the .NET Framework functions for your manipulations, use System.DateTime as the type annotation of the variable.

You can also request that the compiler treat warnings as errors. This helps you catch conversions that are slow and that may cause runtime errors. You can enable warnings by adding the WarningLevel attribute to the Page tag and setting its value. The valid range of values is from 0 (ignore all warnings) to 4 (treat all warnings as errors). For example, to catch all warnings on a page, use a Page tag at the top of your page similar to this one:

<%@ Page Language=JScript WarningLevel=4 %>
JScript object .NET Framework type
ActiveXObject No direct equivalent; similar to System.Object
Array Interoperates with System.Array and typed arrays
Boolean Interoperates with System.Boolean
Date Interoperates with System.DateTime
Enumerator No direct equivalent; obsolete
Error No direct equivalent; similar to System.Exception
Function No direct equivalent; obsolete
Number Interoperates with System.Double
Object Interoperates with System.Object
RegExp Interoperates with System.Text.RegularExpression.Regex
String Data Type
(variable-length)
String and System.String refer to the same data type
String Object
(variable-length)
Interoperates with System.String
VBArray Interoperates with System.Array; obsolete

The JScript Array object deserves special attention. A JScript Array object contains a sparse array that you can extend dynamically. These properties are markedly different from the properties of the System.Array object or typed arrays, which are dense and have a fixed size. Conversions between the two types of arrays can be slow and may involve copying the entire array. In general, you should use System.Array or native arrays unless you need the special properties of JScript Array objects.

Use Classes Instead Of Constructor Functions

JScript .NET (like JScript) supports the use of prototype-based objects. In addition, JScript .NET also allows you to use and define class-based objects. Class-based objects provide type safety, ensure efficient operation, and can extend existing .NET Framework classes. The following basic rules apply to converting prototype-based objects to class-based objects.

Rules for Converting Prototype-based Objects to Class-based Objects

  • Identify the prototype-based objects in your code. You can search for instances of the this keyword, the expando modifier, or the prototype property.
  • Define all classes in a <script runat=server> block. Make sure that the name of the class matches the name of the constructor for the class.
  • Move the constructor function and methods inside the appropriate class and remove the expando modifiers.
  • Define all fields and properties used inside the class. Note that some properties of prototype-based objects are added dynamically as expando properties of the this keyword, while others are added explicitly with the prototype property. If a property or field was added with the prototype property and it should be shared among all instances, define it with the static modifier.
  • Use the class name to type-annotate variables that store instances of class-based objects.

You do not need to follow these rules in order; you can start by placing a class definition around the constructor function and by removing the expando modifier. You can then use the errors reported by loading the page as a guide for additional changes.

To see how this conversion works, consider the example that shows how to define prototype-based objects in ASP.NET pages. The working ASP.NET page is:

<%@ language=jscript %>
<html><body>
<script runat=server>
   // Define the constructor.
   expando function Circle (radius) {
      this.r = radius;
   }
   // Declare an area method for all Circle objects.
   expando function CircleArea () {
      // The formula for the area of a circle is pi*r^2.
      return this.pi * this.r * this.r; 
   }
</script>
<%
   Circle.prototype.pi = Math.PI;
   Circle.prototype.area = CircleArea;
   // Create a new circle.
   var ACircle = new Circle(2);
   // Display the area of the circle.
   Response.Write("Area of the circle is " + ACircle.area() +".<BR>\n");
%>
</body></html>

Here, the expando function Circle is the constructor function for the Circle object. The constructor implicitly adds the r property to the object. The value of r can be different for each instance of the object. In addition, the prototype property explicitly adds the pi property and the area method. Since the pi property is part of the object prototype, the property has the same value for every instance of the Circle object.

You can use the rules to define a Circle class equivalent to the Circle object shown above.

<%@ language=jscript %>
<html><body>
<script runat=server>
class Circle {
   // Define the constructor.
   function Circle (radius : double) {
      this.r = radius;
   }
   // Define the r field.
   var r : double;
   // Define the pi field.
   static var pi : double = Math.PI;
   // Define the area method.
   function area () : double {
      // The formula for the area of a circle is pi*r^2.
      return Circle.pi * this.r * this.r; 
   }
}
</script>
<%
   // Use type annotation, since Circle is a user-defined type.
   var ACircle : Circle = new Circle(2);
   // Display the area of the circle.
   Response.Write("Area of the circle is " + ACircle.area() +".<BR>\n");
%>
</body></html>

A great feature of class-based objects is that the class ties together all the definitions for the class in one place. Prototype-based objects, on the other hand, leave definitions scattered throughout the file. Another benefit is that class-based objects interoperate much better with the .NET Framework than prototype-based objects.

You can use class-based objects in almost the same way as prototype-based objects. The main difference, in terms of upgrading, is that you cannot dynamically add properties to an instance class by default. To enable this feature, you must define the class with the expando modifier. Keep in mind that expando classes use more resources than normal classes (which impacts performance) and that properties dynamically added to the class are not accessible from the .NET Framework.

How to Use JScript .NET Efficiently

Because JScript .NET is a mature and powerful programming language, it offers many choices when creating applications. For example, JScript .NET allows you to concatenate strings by using either the addition (+) operator or the Append method. The optimal way depends on your application, as explained below.

In addition, JScript .NET also provides some choices that are only useful in particular situations. Examples of these are nested functions, the with statement, the eval method, and the Function constructor.

Use the Append Method of the StringBuilder Class

If your code uses the addition (+) operator to perform a large number of string concatenations, performance may improve if you use the Append method of the StringBuilder class instead. This is because the addition (+) operator and the Append method are optimized for different situations; the addition (+) operator is good for concatenating a small number of strings while the Append method is good for concatenating a large number of strings.

The relative performance of the two approaches can depend on several factors, such as machine load, the number of page requests, memory usage, and so on. To determine the best approach for your application, first test the performance of your page with a tool that measures performance with real data. Then inspect your code and replace string concatenations that use the addition (+) operator with concatenations that use the Append method. Retest the application.

The following example demonstrates that there can be a large performance difference between the addition (+) operator and the Append method. The difference is demonstrable by comparing the performance of two pages that concatenate 100 strings. One page uses the addition (+) operator, and the other uses the Append method. In this example, performance is measured with a simple timing technique, which demonstrates that the Append method outperforms the addition (+) operator in this instance.

Note   This example uses a large number of iterations and a simple timing mechanism to measure performance. It is better to use a tool, such as the Microsoft Web Application Stress (WAS) tool, to simulate heavy loads on your actual pages and derive accurate results.
<%@ language=jscript %>
<html><body>
<script runat=server>
   var starttime : Date;
   var s : String;
   var endtime : Date;
</script>
<%
   starttime = new Date();
   // Perform the concatenation 10000 times
   // to get a measurable result.
   for( var j=0; j<10000; j++) {
      s = "";
      // Concatenate 100 strings.
      for( var i=0; i<100; i++)
         s = s + "This is a test! ";
   }
   endtime = new Date();
   Response.Write(s);
   Response.Write("<BR> Milliseconds elapsed: ");
   Response.Write(endtime-starttime);
%>
</body></html>

You can modify the preceding example to use the StringBuilder class instead of the String type and the Append method instead of the addition (+) operator.

<%@ language=jscript %>
<html><body>
<script runat=server>
   var starttime : Date;
   var s : StringBuilder;
   var endtime : Date;
</script>
<%
   starttime = new Date();
   // Perform the concatenation 10000 times
   // to get a measurable result.
   for( var j=0; j<10000; j++) {
      s = new StringBuilder("");
      // Concatenate 100 strings.
      for( var i=0; i<100; i++)
         s.Append("This is a test! ");
   }
   endtime = new Date();
   Response.Write(s);
   Response.Write("<BR> Milliseconds elapsed: ");
   Response.Write(endtime-starttime);
%>
</body></html>

For more information, see StringBuilder Class.

Avoid Nested Functions, the with Statement, the eval Method, and the Function Constructor

You should define functions either at the page level or within class definitions. Make sure that you avoid defining functions within functions to create nested functions. Although nested functions allow you to simplify your code somewhat, they require more resources than normal functions.

To improve performance, you can move nested functions to the page level or class level. If the nested function uses variables defined within the function, you can pass those variables to the nested function as parameters or declare page-level variables that store the needed values.

Note   When the Web server processes an ASP.NET page, it places the contents of the <%...%> blocks into special function definitions. This means that any function defined within a <%...%> block is implicitly a nested function, which should be avoided to achieve optimal performance and to avoid scoping conflicts.

Another construction to avoid is the with statement. In almost all cases, it is more efficient to use a temporary variable to hold the argument of the with statement than to use the with statement itself. For example, you may write:

with (oTable.rows[9].style) { // Inefficient!
   backgroundColor = "#ffff00";
   border = "1px solid #ff0000";
   cursor = "hand";
}

A temporary variable makes the preceding code more efficient and reduces the potential for errors:

var o = oTable.rows[9].style; // More efficient.
o.backgroundColor = "#ffff00";
o.border = "1px solid #ff0000";
o.cursor = "hand";

Finally, the eval method and the Function constructor allow you create and run new code at runtime, providing enormous flexibility for your program. However, each time you create new code, you implicitly call the JScript .NET script engine. In most cases, the execution of the script engine takes much more time than the execution of the new code. Thus, if you can rewrite your program so it does not use the eval method and the Function constructor, you realize a significant performance boost. The changes are usually simple.

Conclusions

After you have upgraded your pages from ASP to ASP.NET, you can enhance performance by using small code changes. For example:

  • Use interop assemblies for COM components or use equivalent .NET components.
  • Use type annotation and user-defined classes.
  • The StringBuilder class may be more efficient than the addition (+) operator.
  • Some features of JScript should not be used unnecessarily.

By applying the changes outlined in this article to your ASP.NET pages, you can achieve a significant performance boost.

Links

ASP.NET Optimization
Discusses the performance improvements offered by ASP.NET and how to take advantage of them.
Microsoft .NET/COM Migration and Interoperability
Describes the interoperability features of the Microsoft .NET Framework that enable developers to continue using ASP pages, COM applications, and Microsoft Win32 DLLs.
Type Library Importer (Tlbimp.exe)
Introduces the Type Library Importer and describes what it does and how to use it.
Interoperating with Unmanaged Code
Describes how the Microsoft .NET Framework interoperates with COM components, COM+ services, external type libraries, and operating system services.
Exposing COM Components to the .NET Framework
Summarizes the process that exposes an existing COM component to managed code.
JScript .NET Language Reference
Lists the essential components of the JScript .NET language.
JScript Language Tour
Introduces and explains the details of the JScript .NET language elements and code syntax.
Page view tracker