ASP developers continually strive to achieve better performance and scalability in their design projects. Fortunately, there are many books and sites that offer great advice in this area. However, these suggestions are based on conclusions drawn from the structure of the ASP platform's work, and there is no quantitative measurement of the actual performance improvement. Because these recommendations require more complex coding and make the code less readable, developers are left to weigh whether improving the performance of their ASP applications is worth the cost without seeing actual results.
This article is divided into two parts. I will present some performance test results to help developers determine whether a particular initiative is worthwhile not only for future projects, but also to update the original project. In the first part I will review some basic issues of ASP development. In the second part, you will cover optimizing some ADO functions and comparing their results with ASP pages that call VB COM objects to perform the same ADO functions. The results are eye-opening and sometimes surprising.
In this article, we will answer the following questions:
* What is the most efficient way to write ASP-generated content into the response stream?
* Should the buffer be enabled?
* Should you consider adding comments to your ASP code?
* Should the default language be set explicitly for the page?
* Should the Session state be closed if not needed?
* Should script logic be placed in subroutines and function areas?
* What are the implications of using include files?
* What kind of load is imposed when performing error handling?
* Does setting up a context handler have any impact on performance?
All testing was conducted using Microsoft's Web Application Focus Tool (WAST), a free tool that can be found here. I created a simple test script using WAST that repeatedly called the ASP page tests described below (over 70,000 times each). The response time is based on the average total time last byte (TTLB), which is the time from the time of the initial request to the time the tool receives the last bit of data from the server. Our test server is a Pentium 166 with 196MB of RAM and the client is a Pentium 450 with 256MB of RAM. You might think that the performance of these machines is not very advanced, but don't forget, we are not testing the capacity of the server, we are just testing the time it takes for the server to process one page at a time. The machines do no other work during the test. The WAST test scripts, test reports, and all ASP test pages are included in the ZIP file for you to review and test yourself.
What is the most efficient way to write ASP generated content into the response stream?
One of the biggest reasons to use ASP is to generate dynamic content on the server. So the obvious starting point for our testing is to determine the most appropriate way to send dynamic content into the response stream. Among the many options, two are the most basic: one is to use inline ASP tags, and the other is to use the Response.Write statement.
To test these options, we created a simple ASP page in which we defined some variables and then inserted their values into a table. Although this page is simple and not very practical, it allows us to isolate and test some individual problems.
Using ASP inline tags
The first test involves using the inline ASP tag < %= x % >, where x is an assigned variable. This method is by far the easiest to perform, and it keeps the HTML portion of the page in a format that's easy to read and maintain.
<% OPTION EXPLICIT
Dim FirstName
DimLastName
Dim MiddleInitial
Dim Address
Dim City
Dim State
DimPhoneNumber
Dim FaxNumber
Dim Email
DimBirthDate
FirstName = John
MiddleInitial = Q
LastName = Public
Address = 100 Main Street
City=New York
State=NY
PhoneNumber = 1-212-555-1234
FaxNumber = 1-212-555-1234
Email = [email protected]
BirthDate = 1/1/1950
%>
<HTML>
<HEAD>
< TITLE >Response Test < / TITLE >
< /HEAD >
<BODY>
< H1 >Response Test < /H1 >
< TABLE >
< tr >< td >< b >First Name:< /b >< /td >< td >< %= FirstName % >< /td >< /tr >
< tr >< td >< b >Middle Initial:< /b >< /td >< td >< %= MiddleInitial % >< /td >< /tr >
< tr >< td >< b >Last Name:< /b >< /td >< td >< %= LastName % >< /td >< /tr >
< tr >< td >< b >Address:< /b >< /td >< td >< %= Address % >< /td >< /tr >
< tr >< td >< b >City:< /b >< /td >< td >< %= City % >< /td >< /tr >
< tr >< td >< b >State:< /b >< /td >< td >< %= State % >< /td >< /tr >
< tr >< td >< b >Phone Number:< /b >< /td >< td >< %= PhoneNumber % >< /td >< /tr >
< tr >< td >< b >Fax Number:< /b >< /td >< td >< %= FaxNumber % >< /td >< /tr >
< tr >< td >< b >EMail:< /b >< /td >< td >< %= EMail % >< /td >< /tr >
< tr >< td >< b >Birth Date:< /b >< /td >< td >< %= BirthDate % >< /td >< /tr >
< /TABLE >
< /BODY >
< /HTML >
The complete code of /app1/response1.asp
Previous best (response speed) = 8.28 msec/page
Use Response.Write statement on each line of HTML
Many of the better learning documents recommend avoiding the previous method. The main reason for this is that, in the process of outputting the page and processing the page to impose latency, if the web server has to convert between sending plain HTML and processing script, a problem called context switching occurs. When most programmers hear this, their first reaction is to wrap each line of raw HTML in a Response.Write function.
…
Response.Write(<html>)
Response.Write(<head>)
Response.Write(<title>Response Test</title>)
Response.Write(</head>)
Response.Write(<body>)
Response.Write(<h1>Response Test</h1>)
Response.Write(<table>)
Response.Write(< tr >< td >< b >First Name:< /b >< /td >< td > & FirstName & < /td >< /tr >)
Response.Write(< tr >< td >< b >Middle Initial:< /b >< /td >< td > & MiddleInitial & < /td >< /tr >)
… <
Fragment of /app1/response2.asp
Previous best (response speed) = 8.28 msec/page
Response time = 8.08 msec/page
Difference = -0.20 msec (2.4% reduction)
We can see that the performance gain from using this approach is very small compared to using inline markup, perhaps because the page loads the server with a bunch of small function calls. The biggest disadvantage of this approach is that since the HTML is now embedded in the script, the script code becomes more verbose and harder to read and maintain.
Use wrapper functions
Perhaps the most discouraging discovery when trying to use the Response.Write statement approach is that the Response.Write function cannot place a CRLF at the end of each line. So when you read the source code from the browser, the HTML that was perfectly laid out is now one line without an end. I think your next discovery may be even more horrifying: there is no sister function Writeln in the Response object. So, an obvious reaction is to create a wrapper function for the Response.Write function to append a CRLF to each line.
…
writeCR(< tr >< td >< b >First Name:< /b >< /td >< td > & FirstName & < /td >< /tr >)
…
SUB writeCR(str)
Response.Write(str & vbCRLF)
END SUB
Fragment of /app1/response4.asp
Previous best (response speed) = 8.08 msec/page
Response time = 10.11 msec/page
Difference = +2.03 msec (25.1% increase)
Of course, since this approach effectively doubles the number of function calls, its impact on performance is significant and should be avoided at all costs. Ironically CRLF also adds 2 bytes per line to the reactive stream, which the browser does not need to render to the page. All well-formatted HTML does is make it easier for your competitors to read your HTML source code and understand your design.
Concatenate consecutive Response.Write into a single statement
Regardless of our previous tests with wrapper functions, the next logical step is to extract all the strings from the separate Response.Write statements and concatenate them into a single statement, thus reducing the number of function calls , greatly improving the performance of the page.
…
Response.Write(<html>&_
< head > & _
<title>Response Test</title> & _
< /head > & _
<body> & _
< h1 >Response Test< /h1 > & _
< table > & _
< tr >< td >< b >First Name:< /b >< /td >< td > & FirstName & < /td >< /tr > & _
…
< tr >< td >< b >Birth Date:< /b >< /td >< td > & BirthDate & < /td >< /tr > & _
< /table > & _
< /body > & _
< /html >)
Fragment of /app1/response3.asp
Previous best (response speed) = 8.08 msec/page
Response time = 7.05 msec/page
Difference = -1.03 msec (12.7% reduction)
Currently, this is the most optimized configuration.
Concatenate consecutive Response.Write into a single statement, adding a CRLF at the end of each line
In consideration of those who require their source code to look pristine from a browser, I used the vbCRLF constant to insert some carriage returns at the end of each line in the previous test and reran it.
…
Response.Write(<html>&vbCRLF&_
< head > & vbCRLF & _
< title >Response Test< /title > & vbCRLF & _
< /head > & vbCRLF & _
…
Fragment of /app1/response5.asp
Previous best (response speed) = 7.05 msec/page
Response time = 7.63 msec/page
Difference = +0.58 msec (8.5% increase)
The result is a slight decrease in performance, perhaps due to the extra concatenation and the increased number of characters.
Review and Observation
Some rules can be drawn from the previous tests on ASP output:
* Avoid excessive use of inline ASP.
* Always concatenate consecutive Response.Write statements into a single statement.
* Never use wrapper functions around Response.Write to append CRLF.
* If HTML output must be formatted, append CRLF directly within the Response.Write statement.
Should the buffer be enabled?
Start buffer via script
By including Response.Buffer=True at the top of the ASP script, IIS will cache the content of the page.
<% OPTION EXPLICIT
Response.Buffer = true
Dim FirstName
…
Fragment of /app1/buffer__1.asp
Previous best (response time) = 7.05 msec/page
Response time = 6.08 msec/page
Difference = -0.97 msec (13.7% reduction)
Performance has been greatly improved. But wait, there’s something better.
Start buffer via server configuration
Although the buffer is enabled by default in IIS 5.0, it must be enabled manually in IIS 4.0. Now find the site's Properties dialog box and select the Configure button from the Home Directory tab. Then select enable buffering under App options. For this test, the Response.Buffer statement was moved from the script.
Previous best = 7.05 msec/page
Response time = 5.57 msec/page
Difference = -1.48 msec (21.0% reduction)
Currently, this is the fastest response we have ever had, 21% lower than our previous best-case response time. From now on, our future tests will use this reaction time as the baseline value.
Review and Observation
Buffers are a great way to improve performance, so setting buffers to the server's default values is necessary. If for some reason the page cannot properly run the buffer, just use the Response.Buffer=False command. One disadvantage of buffers is that the user sees nothing from the server until the entire page has been processed. Therefore, during the processing of complex pages, it is a good idea to occasionally call Response.Flush to update the user.
Now we have one more rule added: always enable buffering via server settings.
Should you consider adding comments to your ASP code?
Most HTML developers know that including HTML comments is a bad idea, firstly it increases the size of the data being transferred, and secondly they just provide other developers with information about the organization of your page. But what about comments on ASP pages? They never leave the server, but they do increase the size of the page, so they must be broken up using ASP.
In this test, we added 20 comments, each with 80 characters, for a total of 1,600 characters.
<% OPTION EXPLICIT
'------------------------------------------------ ----------------------------------
…20 lines…
'------------------------------------------------ ----------------------------------
Dim FirstName
…
/app2/comment_1.asp fragment
Baseline = 5.57 msec/page
Response time = 5.58 msec/page
Difference = +0.01 msec (0.1% increase)
The results of the test were astonishing. Although comments are almost twice as long as the file itself, their presence does not have a huge impact on response times. So we can follow the following rules:
As long as used moderately, ASP annotations have little or no impact on performance.
Should the default language be set explicitly for the page?
IIS handles VBScript by default, but I see that in most cases the language is explicitly set to VBScript using the <%@LANGUAGE=VBSCRIPT%> statement. Our next test will examine what impact the presence of this declaration has on performance.
< %@ LANGUAGE=VBSCRIPT % >
<% OPTION EXPLICIT
Dim FirstName
…
/app2/language1.asp fragment.
Baseline = 5.57 msec/page
Response time = 5.64 msec/page
Difference = +0.07 msec (1.2% increase)
As you can see, including language declarations has a slight impact on performance. therefore:
* Set the server's default language configuration to match the language used on the site.
* Do not set a language declaration unless you are using a non-default language.
Should the Session state be turned off if not needed?
There are many reasons to avoid using the IIS Session context, and those could be their own article. The question we are trying to answer now is whether closing the Session context when it is not needed by the page will help improve performance. Theoretically, it should be yes, because then there is no need to use the page to instantiate the Session context.
Like the buffer, there are two ways to configure the session state: through scripts and through server settings.
Close Session context via script
For this test, to close the Session context in the page, I added a Session state declaration.
< %@ ENABLESESSIONSTATE = FALSE % >
<% OPTION EXPLICIT
Dim FirstName
…
/app2/session_1.asp fragment.
Baseline = 5.57 msec/page
Response time = 5.46 msec/page
Difference = -0.11 msec (2.0% decrease)
Good progress has been made through such a small effort. Now look at part two.
Close the Session context through server configuration
To close the Session context on the server, go to the site's Properties dialog box. Select the Configuration button on the Home Directory tab. Then uncheck enable session state under App options. We run the test without the ENABLESESSIONSTATE statement.
Baseline = 5.57 msec/page
Response time = 5.14 msec/page
Difference = -0.43 msec (7.7% reduction)
This is another significant improvement in performance. Therefore, our rule should be: always close the Session state at the page or application level when it is not needed.
Will using Option Explicit substantially change performance?
Set Option Explicit at the top of an ASP page to require that all variables be declared on the page before use. There are two reasons for this. First, applications can process variable access faster. Secondly, this prevents us from accidentally using variable names incorrectly. In this test we remove the Option Explicit reference and the Dim declaration of the variable.
Baseline = 5.57 msec/page
Response time = 6.12 msec/page
Difference = +0.55 msec (9.8% increase),
Even though some lines of code were removed from the page, response times still increased. So although using Option explicit is sometimes time-consuming, it has a significant effect on performance. So we can add another rule: always use Option explicit in VBScript.
Should script logic be placed in subroutines and function areas?
Using functions and subroutines is a good way to organize and manage code, especially when a code area is used multiple times on the page. The disadvantage is that it adds an extra function call to the system that does the same job. Another problem with subroutines and functions is the scope of variables. In theory, it is more efficient to specify variables within a function area. Now let's see how these two aspects come into play.
Move the Response.Write statement into a subroutine
This test simply moves the Response.Write statement into a subroutine area.
…
CALL writeTable()
SUB writeTable()
Response.Write(<html>&_
< head > & _
…
< tr >< td >< b >EMail:< /b >< /td >< td > & EMail & < /td >< /tr > & _
< tr >< td >< b >Birth Date:< /b >< /td >< td > & BirthDate & < /td >< /tr > & _
< /table > & _
< /body > & _
< /html >)
END SUB
/app2/function1.asp fragment
Baseline = 5.57 msec/page
Response time = 6.02 msec/page
Difference = +0.45 msec (8.1% increase)
As expected, subroutine calls place additional overhead on the page.
Move all scripts into subroutines
In this test, the Response.write statement and variable declarations are moved into a subroutine area.
<% OPTION EXPLICIT
CALL writeTable()
SUB writeTable()
Dim FirstName
…
DimBirthDate
FirstName = John
…
BirthDate = 1/1/1950
Response.Write(<html>&_
< head > & _
<title>Response Test</title> & _
< /head > & _
<body> & _
< h1 >Response Test< /h1 > & _
< table > & _
< tr >< td >< b >First Name:< /b >< /td >< td > & FirstName & < /td >< /tr > & _
…
< tr >< td >< b >Birth Date:< /b >< /td >< td > & BirthDate & < /td >< /tr > & _
< /table > & _
< /body > & _
< /html >)
END SUB
/app2/function2.asp fragment
Baseline = 5.57 msec/page
Response time = 5.22 msec/page
Difference = -0.35 msec (6.3% reduction)
Very interesting! Although moving variables into function scope adds an extra function call, it actually improves performance. We can add the following rules:
* On a page, if the code is to be used more than once, enclose the code in a function area.
* When appropriate, move variable declarations into function scope.
What are the implications of using include files?
An important feature of ASP programming is the inclusion of code from other pages. With this feature, programmers can share functions across multiple pages, making code easier to maintain. The disadvantage is that the server must assemble the page from multiple sources. Below are two tests using Include files.
Include files using inline code
In this test, a small piece of code has been moved into an Include file:
<% OPTION EXPLICIT
Dim FirstName
…
DimBirthDate
FirstName = John
…
BirthDate = 1/1/1950
%>
< !-- #include file=inc1.asp -- >
/app2/include_1.asp fragment
Baseline = 5.57 msec/page
Response time = 5.93 msec/page
Difference = +0.36 msec (6.5% increase)
This is not surprising. The payload is formed using Include files.
Use Include files in the function area
Here, the code is wrapped in a subroutine in an Include file. The Include reference is made at the top of the page and calls the subroutine at the appropriate location in the ASP script.
<% OPTION EXPLICIT
Dim FirstName
…
DimBirthDate
FirstName = John
…
BirthDate = 1/1/1950
CALL writeTable()
%>
< !-- #include file=inc2.asp -- >
/app2/include_2.asp fragment
Baseline = 5.57 msec/page
Response time = 6.08 msec/page
Difference=+0.51 msec (9.2% increase)
This has a greater impact on performance than functions calls. So: Use Include files only when code is shared between pages.
How much load is created when performing error handling?
Error handling is necessary for all real applications. In this test, the error handler is called by calling the On Error Resume Next function.
<% OPTION EXPLICIT
On Error Resume Next
Dim FirstName
…
/app2/error_1.asp fragment
Baseline = 5.57 msec/page
Response time = 5.67 msec/page
Difference = 0.10 msec (1.8% increase)
As you can see, error handling comes with a price. We can suggest the following: Use error handles only when something happens that is beyond your ability to test or control. A basic example is using COM objects to access other resources, such as ADO or FileSystem objects.
Does setting up a context handler have any performance impact?
When an error occurs, setting a context handler on the page allows the script to reverse the action. This is set using a processing statement on the page.
< %@ TRANSACTION = REQUIRED % >
<% OPTION EXPLICIT
Dim FirstName
…
/app2/transact1.asp fragment
Baseline = 5.57 msec/page
Response time = 13.39 msec/page
Difference = +7.82 msec (140.4% increase)
ah! This is truly the most dramatic result. So please note the following rule: Use processing context only when two or more operations are performed as a unit.
in conclusion
What’s important about the first part of this article is the accumulation of many little things. To highlight this issue, I set up a final test where I did all the things we'd tested before that seemed innocuous but actually had a bad effect. I included a bunch of Response.Write statements, turned off the buffer, set the default language, removed the Option Explicit reference, and initialized the error handler.
< %@ LANGUAGE=VBSCRIPT % >
< %
On Error Resume Next
FirstName = John
…
BirthDate = 1/1/1950
Response.Write(<html>)
Response.Write(<head>)
Response.Write(<title>Response Test</title>)
Response.Write(</head>)
Response.Write(<body>)
Response.Write(<h1>Response Test</h1>)
Response.Write(<table>)
Response.Write(< tr >< td >< b >First Name:< /b >< /td >< td > &_
FirstName & < /td >< /tr >)
…
Response.Write(< tr >< td >< b >Birth Date:< /b >< /td >< td > &_
BirthDate & < /td >< /tr >)
Response.Write(</table>)
Response.Write(</body>)
Response.Write(</html>)
%>
/app2/final_1.asp fragment
Baseline = 5.57 msec/page
Response time = 8.85 msec/page
Difference = +3.28 msec (58.9% increase)
It may sound obvious, but it's more important to understand that the code we place on the page has an impact on performance. Small changes on the page can sometimes significantly increase response times.
Summary of rules
* Avoid excessive use of inline ASP.
* Always concatenate consecutive Response.Write statements into a single statement.
* Never use wrapper functions around Response.Write to append CRLF.
* If HTML output must be formatted, append CRLF directly within the Response.Write statement.
* Always enable buffering via server settings.
* As long as used moderately, ASP annotations have little or no impact on performance.
* Set the server's default language configuration to match the language used on the site.
* Do not set a language declaration unless you are using a non-default language.
* Always use Option explicit in VBScript.
* Always turn off Session state at the page or application level when not needed.
* Use Include files only when code is shared between pages.
* On a page, if the code is to be used more than once, enclose the code in a function area.
* When appropriate, move variable declarations into function scope.
* Use error handles only if conditions occur that are beyond the capabilities of the test or control.
* Use context processing only when two or more operations are performed as a unit.
Looking back now, there are a number of questions that can serve as general guidelines:
* Avoid redundancy--don't set properties that are already set by default.
* Limit the number of function calls.
* Reduce the scope of the code.