|
Note: this
is my old ASP tips page and is no longer supported. See the new
Visualize web site.. This
part of the site is for any ASP developer who wishes to get the most from
their ASP scripting. It presents a number of hints and tips which I have
produced as a result of developing various ASP applications over the last
few years or so. Most of the tips are of relevance to any platform that
supports ASP, whether it be Microsoft ones or those ported to others by
third-part vendors (e.g. Chillisoft, Instant ASP).
VBScript is used in the examples, as this is the most popular scripting
language chosen on the server-side, but most of the ideas are just as
applicable to JScript. The popularity of VBScript will no doubt change
in the future with ASP.Net though, with C# being a nicer language and
possibly likely to become the chosen language for most developers. Based
on what I've seen in beta 2 I can't wait!
Note these tips are not aimed at ASP.Net
Many of the tips assume knowledge of basic ASP scripting and general
database connectivity issues, and novice ASP developers should use these
tips in conjunction with the numerous basic ASP scripting guides now available
on the web. See my ASP external links page
as a starting point.
The goals of these pages are:
- To provide new ASP developers with valuable information on how to
write good ASP
- Provide a useful checklist of hints and tips for writing efficient
and maintainable ASP
- Encourage consistency of coding across web sites and applications
By following these hints and tips, your applications will be more responsive,
efficient on server resources and easier to modify and maintain.
This page will be updated regularly, as and when I have a tip that is
worth sharing. If you have any other hints/tips which you think would
be useful for adding to this page, email
me now and I will check the hint/tip and add it to the site.
The page is broken up into the following sections:
indicates an IIS5/Windows 2000 only tip
DISCLAIMER: Note these pages are a free resource for anyone wishing to reference them. Although every care is taken to ensure their correctness, the author takes no responsibility for any errors or problems that may occur through their use, or indeed misuse. These pages are copyight of Dave Clarke, Visualize Software Ltd 1997-2000 (all rights reserved).
Structuring your Scripts
All ASP scripts, other than very trivial ones, are worthwhile structuring
into procedures, functions and SSIs (server-side includes). This will aid
maintenance and readability considerably. A typical script could have the
following structure:
<% Option Explicit ' No implicit declarations thank you!
' This should appear at the top of all
' scripts to enforce variable declaration
%>
<!--#INCLUDE FILE='YourStandardIncludesHere.inc'--<
<!--#INCLUDE FILE='etc.inc'--<
<%
' Now declare any script level constants
Const sMYCONSTANTSTRING = "This remains constant"
Const sMYCONSTANTSTRING2 = "etc..."
' Now declare any script level variables
Dim iMyInteger, sMyString, bMyBoolean
' Now instantiate any script level objects
' (ADO dbConns, dictionary object etc)
' connect to database here
Set oDBConn = Server.CreateObject("ADODB.Connection")
oDBConn.Open "TestDSN"
' main processing here
' .... etc
' now close any database connections and free up objects
oDBConn.Close
Set oDBConn = Nothing
'***************************
' Private subroutines/functions here.
' Keep variables local where possible
'***************************
' have any standard footers if necessary here
<!--#INCLUDE FILE='YourStandardFooterIncludesHere.inc'--<
Obviously, the structure of the script will vary slightly depending
on the nature of the script, but this is a good general rule of thumb.
All includes (containing useful routines and commonly used code) are marked
clearly at the top, it's clear which variables are at script level, and
the main processing is near the top, hence easy to find.
Note the symmetry in the code, i.e. create, open, process, then close
and free up memory. By keeping symmetry like this, you won't go far wrong
on performance and efficiency of server resources.
Finally, I tend to keep my HTML output routines totally separate from
the main logic, usually via a display routine at the bottom of the page
or in a separate include, thus keeping presentation separate from the
business logic.
Consistency is the key here. Variable naming conventions are usually left
to the developer, and each will have their own preferences as to what "works
for them" etc. However, from a team maintenance point of view, it is well
worth adopting a standard within teams where possible. My personal preference
is the hungarian style notation, which is well established and can be used
across a number of languages. The big plus with this approach is that you
can immediately tell the type of data that is being stored in each variable,
just from seeing its name (rather than having to track down its declaration).
As you are likely to be aware, ASP uses Variants and does not explicitly
declare variables of particular types. Even though this is true, it is still
extremely useful to know what is intended to be stored in the variable,
and hence this notation is worth considering for ASP. Take it from me, once
you get used to using this approach, it will become a very useful habit!
The notation adopts a lowercase prefix, indicating the data type of
the variable concerned, followed by the name of the variable itself, often
capitalising each word within it (no spaces). Constants are similar, but
typically we only lowercase the prefix, capitalising the rest of the name
to clearly distinguish these from normal variables. Below provide some
typical examples:
| Variable Name |
Data Type |
Prefix |
| iPeopleCount |
integer |
i |
| bEndOfFile |
boolean |
b |
| sName |
string |
s |
| oMyObject |
Object |
o |
| rsMyRecordset |
Recordset Object |
rs |
| iFIXEDSIZE |
integer constant |
i |
Obviously, this list can be extended (e.g. col for collections, txt
for HTML text boxes etc.), but I'll leave that to you.
For information that rarely changes and needs application scope (e.g. look-up
info, constants), consider putting these into the Application object. Such
values will be cached and be available to all scripts within the application,
as well as acting as a central point for maintenance at a later stage.
A classic bottleneck this one on servers, and one which can rapidly eat
resources if not handled correctly. Ensure that all recordsets and database
connections are closed correctly after use, and objects are set =NOTHING
where relevant. Generally, this should occur on the same page as they are
created/opened (keep code symmetry), which will make best use of the database
connection pooling on the server (if using ODBC, make sure connection pooling
is enabled in the ODBC driver manager - if the option is not present, it
will be worth your while upgrading MDAC on the server, see Data
Access Components/ODBC drivers). Example code structure:
Dim dbConn, RS, sSQL
Set oDBConn = Server.CreateObject("ADODB.Connection")
oDBConn.Open "TestDSN"
sSQL = "SELECT Field1, Field2 " &_
"FROM MyTable "
set rsTest = dbConn.Execute(sSQL)
' .... check for SQL error and process results here
' .... etc
rsTest.Close
oDBConn.Close
Set rsTest = Nothing ' Free up memory
Set oDBConn = Nothing
Place ODBC DSN or OLEDB connection strings in either include files or in
the application object. This ensures they are only stored in one place,
aiding maintenance at a later date.
All SQL executions should have some form of error handling, and call a standard
error handler in an include file.
Example code could be:
<!--#INCLUDE FILE='error.inc'--><%
Dim oDBConn, rsTest, sSQL
Set oDBConn = Server.CreateObject("ADODB.Connection")
oDBConn.Open "TestDSN"
sSQL = "SELECT Field1, Field2 " &_
"FROM MyTable "
On error resume next 'catch ODBC/SQL errors in a minute
Set rsTest = oDBConn.Execute(sSQL)
On error goto 0 'error handling back on
' now check for ODBC errors
IF oDBConn.Errors.Count <> 0 then
Call MyErrorHandler(oDBConn, sSQL) ' pass database connection as param
Else
'.... process results
'.... etc
End If
rsTest.Close
oDBConn.Close
Set rsTest = Nothing ' Free up memory
Set oDBConn = Nothing
and the actual error handler function itself in the include file can take
many forms. As a minimum it could simply output details of the error in
the page itself, for example:
Public Sub MyErrorHandler(oDBConn, sSQL)
Response.Write "An error has occurred.<BR>"
Response.write "SQLstate=" & oDBConn.Errors(0).sqlstate & "<BR>"
Response.write "Description=" & oDBConn.Errors(0).Description & "<BR>"
Response.write "NativeError=" & oDBConn.Errors(0).NativeError & "<BR>"
Response.write "SQL=" & sSQL & "<BR>"
Response.Write "Please telephone the Web Support Team on tel no xxxx"
On Error Resume Next
oDBConn.Close
Set oDBConn = nothing
On Error goto 0
Response.End
End Sub
Of course, this routine can be as complicated as you wish. In the past I
have written error handlers which initially redirects to a neat user-friendly
form. This then gathers some additional user details and informs the web
team automatically via email, with error logging, giving full technical
details of the error, all hidden from the end-user.
To aid portability in your scripts avoid using literal file paths in your
code and use MapPath instead. There is a small performance penalty
for this, as this method requires the web server to retrieve the current
server path. Hence, it is recommended to call this once and place the result
into a variable for referencing later where possible.
When debugging asp pages, if you have set the response.buffer = true, you
will not always see ASP error messages. Hence, set the response.buffer =
false during debugging - this ensures that if the page cannot be processed
(because of an ASP coding problem for example), an error message will appear.
See also: Buffering output for speed
When looping in ASP, it makes sense to avoid looping more times than is
necessary and exit the loop as soon as you can. This can normally be accomplished
by having appropriate conditions on the loop itself, for example:
dim i
i=0
do while i < 10 and not rs.eof
' process your recordset here...
rs.movenext
i = i + 1
loop
Occasionally, it is sometimes useful to exit a loop early, even if the main
loop conditions are not met. VBscript offers the Exit For and Exit
Do statements for just this purpose. For example, if we wanted to add
some code which exits the loop if some error arises, then we could write
this as follows:
i=0
do while i < 10 and not rs.eof
' process your recordset here...
If [some error occured] then
Exit Do
end if
rs.movenext
i = i + 1
loop
Generally, it is good coding practice to put the conditions on the loops
themselves, as opposed to using the exit statements illustrated above. However,
the exit statement is extremely useful for exiting if a special case arises.
This avoids the need to have extra boolean flags etc.
Storing a component in the session or application object often seems the
ideal thing to do, ie we simply set up the object "once" at session or application
start up, and this is cached until we need it - no need to keep creating
it and destroying it on each page and this should result in excellent performance.
This is fine, and is a good idea, providing the component is designed
to be used in this way. Many are not however, including those written
in Visual Basic 6!
The only components that should be used in this way are those classed
as "truly free-threaded" (or "agile" in Microsoft speak). These are written
to allow multiple-threads to run safely, concurrently within the component,
catering for all the issues that can arise, such as mutual exclusion problems,
lockout or deadlock situations and controlling access to shared data.
Hence, only store components at session or application level if you
know that the component is truly multi-threaded. Those created with VB6
(or below) will not be and should therefore only be used at the page level.
For sites with very high hit rates, for non-dynamic pages use .htm rather
than .asp extensions. The web server can serve these static pages at a more
rapid rate, as they do not need parsing before sending to the client browser.
This takes some load off the server. If necessary, such pages can be created
in batch via some ASP (or other tool) routines, providing a half way house
between true dynamic pages and limited, manually edited, static ones. Such
routines could be run daily, weekly etc.
If you are not sure whether an object is actually going to be used or not
in a script, then use the <OBJECT> tag instead of Server.CreateObject().
This will declare an object without actually instantiating it (ie instead
it will wait until it is referenced, termed 'lazy evaluation'). Another
benefit is they use CLASSIDS, which eliminate name collisions due to their
uniqueness.
Declare a number of variables per line, using the Dim statement. This results
in less lines to parse. e.g. replace
Dim sMyString
Dim sMyTemp
Dim iCounter
with:
Dim sMyString, sMyTemp, iCounter Use local variables
whenever possible. This not only aids maintenance by localising control,
but also speeds up processing since the entire namespace does not have
to be searched when the local variable is referenced. Also, copy individual
collection values into local variables, if you are going to reference
them a number of times (saves multiple lookups). Avoid redimming arrays.
Instead, it's probably more efficient to declare the overall max size
in advance.
Avoid lots of
<% and %>
context switches between ASP and HTML in your pages. This slows down the
parsing and makes code unreadable. It's much better to group your HTML and
ASP into fair sized blocks, thus minimising the amount of context switches
required per page.
lthough this appears an obvious one, I still see the odd ASP script around
which uses ASP loops to search through ADO recordsets for a particular record(s)
to display, when really the SQL query should have done the searching. Avoid
looping through recordsets for searching purposes, unless there is a specific
reason to do so.
Worse still, avoid requerying databases inside ASP loops to carry out
look-up operations. These have a high overhead and 9 times out of ten,
are often not required. It's much better to make appropriate use of SQL
inner/outer joins, even correlated sub-queries if necessary etc., to carry
out such searches efficiently at the database engine level (that's what
databases are good at!), leaving the ASP to control final displaying of
the results.
Rather than use pass-through SQL, consider setting up stored procedures
to handle common queries. These are more efficient and robust than constructing
and executing full SQL statements. Most database engines offer these facilities
including DB2, ORACLE (can be combined with PL/SQL) and SQL Server.
Only use sessions where needed. They are essentially glorified "global variables"
and as anyone with a background in software engineering will tell you -
"globals are bad news". There are a number of reasons for this. Not only
do they tightly couple your scripts together, raising all kinds of maintenance
issues (we should aim for loosely coupled pages), they also reduce scaleability
as they tie a user to that specific server, making the use of web farms
and load balancing more difficult. Sessions also require cookies to be enabled
on the browser for session id information to be stored. Something we cannot
always guarantee. Also, sessions can use a great deal of the server's resources,
especially if abused, as a session typically lasts for around 20 minutes
(default session timeout), wasting valuable memory, often long after the
user has left the site! The worst scenario for this is where objects, recordsets
or arrays are stored in a session and are "never" explicitly closed or destroyed.
Oh dear... that poor server...
A final thought on this - if you really need to store a recordset or
other object in a session (and there are times when this is useful), as
a catch all, please ensure that Session_OnEnd() in global.asa has the
relevant code to close them and free up memory afterwards. Also, minimise
the session timeout to as low a figure as is acceptable, and add the @ENABLESESSIONSTATE=False
directive to all pages that do not require session state information (saves
web server resources):
This object is often overlooked and can often be an excellent replacement
for hand-crafted arrays. It provides an efficient look-up facility, via
key/data pairs. Of course, don't forget to explicitly destroy the object
after use by setting = nothing. ;-)
Finally, as tempting as it may seem, avoid storing this object
at application level. The component is not truly free-threaded,
so doing so will result in unpredictable results and data corruption may
well result. This is a shame, as in many situations having the dictionary
at application level would be very useful. Perhaps Microsoft will fix
this in a future release.
See also Beware of storing components at sesson/application
level.
If you have routines that are used by many scripts, are fairly processor
intensive in nature or contain a fair amount of business logic, consider
compiling these up into COM objects/ActiveX DLLs, using tools such as VB,
Delphi or VC++. These compiled routines will run faster than interpreted
ASP and are often easier to maintain. However, do remember that instantiating
objects in ASP, do have some overhead, which should be taken into consideration
when designing your scripts. Once created however, calling the various methods
should be very efficient. As a basic rule, "create once, call many times,
destroy (using =nothing) after use."
When an ASP script is run, it is given a fixed amount of time to complete
processing before it will time out. The length of time before this happens
can be set in two ways, either via the MMC, or by using Script.Timout in
the code. By default this is set to 90 (seconds), but can be altered as
necessary on the fly in code. As a basic rule of thumb, leave it set to
the default in most cases, but on occasions it may be required to increase
it slightly (say to 180 seconds) for a large script to complete processing.
If this is the case, the script timeout should be set at the beginning of
the script concerned, and then reset to the default at the end. Of course,
one should probably be writing a component for tasks such as these...
This is useful to use to ensure that the client is still fetching the page
that is currently processing. For example, consider the following classic
loop which simply goes through all the records in a recordset:
Do While Not rsTest.EOF
' process and display each record
' etc....
rsTest.MoveNext
Loop
Let's say there are 5000 records to process. What if the user stops the
page before it is fully finished - the script will continue to run, when
the user has long gone! The following rectifies the situation:
Do While (Not rsTest.EOF) And (Response.IsClientConnected)
' process and display each record
' etc....
rsTest.MoveNext
Loop
This will loop until end of file or the user selects the stop button. Excellent,
just what we want. ;-)
Accessing a server variable for the first time requires a special request
to the server, to collect ALL the variables, not just the one you're interested
in, so make these requests with care. Subsequent requests are not such a
performance overhead.
When using recordsets/cursors, ensure you fully understand the types you
are requesting. These are documented in the standard ASP Roadmap on-line
documentation. The vendor specific documentation regarding specific drivers/DLLs
should also be fully understood to get the most from database performance.
Consider buffering the output generated from an ASP page by using response.buffer
= true. HTTP and TCP/IP work much more efficiently when sending decent
sized chunks of data. Sending tiny chunks is hence expensive.
Having said that, if the page is quite large and buffering is on, the
end-user may think the page is responding poorly as they will not see
any output in the browser until the page has finished. If this is relevant
to your page, do a response.flush to flush the buffer every so
often, at appropriate points in the code, to provide some feedback to
the user on page progress.
It is often useful to redirect to a different page from within your script.
In IIS4 and ASP2 you would typically use response.redirect to do
this. This would send a response to the browser to instruct it to request
the new page - thus the browser has essentially made two page requests to
the server.
IIS5/ASP3 improves on this, by providing a server-side approach to redirection,
namely server.transfer, by transferring execution immediately to
another page directly, avoiding the "extra round-trip" approach in IIS4.
Much more efficient.
Logging page access obviously has some overhead associated with it, so it
makes sense to only log those directories you're interested in. You can
do this by deselecting the log access option in the MMC (Microsoft Management
Console) for the directory concerned.
When you define an ASP application in the Microsoft Management Console (MMC),
there is a "run is separate memory space" check box, which is often overlooked,
and allows ASP applications to be run as a separate process to IIS (inetinfo).
So why would you want to do this? Typically you would leave this unchecked,
thus allowing the application to run in the same space as IIS and process
very efficiently. However, by doing this you are potentially allowing a
rogue application to overwrite some memory being used by IIS, potentially
crashing inetinfo, therefore bringing down the whole web server. Hence,
if you have any ASP applications that have not been fully tested, or are
known to be "dodgy" in nature (there is always one out there!), then, at
the sacrifice of losing a little performance, you should seriously consider
running these in their own space. Reliability is crucial in most cases,
and this is one simple way of increasing it.
See also: IIS5 Application Settings
If you're lucky enough to be setting up an IIS NT4 server from scratch,
then it's important to get settings such as the locale/regional set up correctly
very early on, before ASP scripts are developed on them, as they can effect
how scripts behave, particularly those which rely on system date/time information.
Anyway, to cut a long story short, log on locally as administrator and check
that the regional settings are as expected. In particular, check that the
Short Date setting is as you require. For example, in the UK, we
would typically want the short date format to show as dd/mm/yyyy,
but the default is US format with a two digit year (ie m/d/yy). This can
create havoc with scripts which use the ASP date() function which uses the
locale setting for its formatting (writing your own date component would
be one way of tackling this issue).
Make sure that you also change the regional settings for IUSR_machine
etc. as well. There are two ways of doing this, either by logging on as
each in term (you will need to set or know the passwords), or by searching/editing
the registry for sShortDate and modifying them directly (only do this
if you are confident with regedit!).
Check also that NT's roaming profiles do not affect the date settings
when other users log on to the machine (simply write a short ASP script
which displays the result returned from date(), keep refreshing
the page while other users log-on to the server, and see if the date format
changes). If the users are set up correctly, it shouldn't.
There also appears to be a bug in NT4 SP3 and IIS4. Despite changing
all occurrences of ShortDate in the registry, the date format that you
specify does not appear to take effect after a fresh reboot. It only works
once at least one user has logged onto the machine and off again. Strange...
if someone knows the answer to this, please drop me
a line....
If you're still having problems, you should also try adding "Session.LCID
= 3081" (eg is for UK) in the script. You can also use this approach to
use the user's browser language settings captured from the HTTP header
info. See this
Microsoft Multi-language Support KB article for more details. This
also contains the codes for other countries.
It is important to keep tabs on the latest drivers that are used to handle
all the backend database connections. For example, later versions of MDAC
(Microsoft Data Access Components) introduced improved features, including
connection pooling.
As a starting point, keep any eye on the Microsoft data access site
at http://www.microsoft.com/data,
and consider installing the latest versions of drivers when needed. The
new drivers may well fix many of the problems you have been recently having
(of course, they may well also introduce some new ones at the same time!
Thorough testing is the only safe approach).
Note. Later versions of MDAC moved towards a more standard ANSI
SQL syntax, hence you may well find that on upgrading, some of your old
MS Access SQL statements that do not follow the ANSI format, will provide
unpredictable results. For example:
WHERE fieldname = NULL
would work fine with older drivers, but under the new ones, will probably
return an empty recordset. Instead use the correct syntax of:
WHERE fieldname IS NULL
MS Access is not the ideal database system on which to build scaleable web
based applications. 'True' multi-threaded DBMSs such as SQL Server, Oracle
or DB2 are much better suited to medium to large scale development. However,
the convenience of MS Access and also the likelyhood of having to 'tap into'
existing application data can not be overlooked, hence this makes it highly
probable that as some time we will want to connect to this (what is essentially
a desktop) DBMS. To be fair, MS Access often holds up very well (on small
intranets for example), considering some of the conditions it is often put
under.
Tips? Make sure you have up to date drivers (see previous point) which
may offer better performing and more robust connection mechanisms. Also,
if you are experiencing memory/performance problems through ODBC, try
tweaking the following settings in the ODBC data source administrator:
- Select relevant Access 97 DSN on the System DSN tab
- Select Configure, then Advanced.
- Select MaxBufferSize and change the value to 8192 (512 is the default)
- Select Threads, and change the value to 20 (3 is the default)
- Confirm all the changes and exit the administrator
These new settings should provide you with better performance in most cases.
Try increasing them slightly more if necessary, depending upon the resources
you have available.
Finally, if you are still using ODBC you should really consider connecting
directly using OLEDB. An example connection string could be:
"Provider=Microsoft.Jet.OLEDB.4.0; Data Source=d:\websites\yourdir\yourdb.mdb;"
This is more efficient and reliable than ODBC.
To protect your ASP scripts from being downloaded by a user appending ::$DATA
to the script name in the URL, install the fix from Microsoft available
at:
http://www.microsoft.com/security/bulletins/ms98-003.asp. If you are
using NT4 SP4 or above, then you should not have a problem, but it's worth
checking.
IIS5 gives you more scope when setting up web applications. In IIS4 you
basically had a choice to either run a web application "in process" with
inetinfo (IIS process) giving best performance, but at the risk of a "dodgy"
application corrupting the IIS process itself. The alternative was to choose
"run in separate memory space", which gave better robustness, but at the
expense of poorer performance.
IIS5 introduces the term "isolation" levels, which provides better control
over your web applications:
- Low Isolation. As IIS4 "in process". Best performance, but at the
expense of the possibility of a rogue application bringing the service
down.
- Medium. The default in IIS5, and is a new IIS5 only level. ASP processes
share a single process space, but outside of IIS itself.
- High. As IIS4 "out of process". Provides the best reliability, at
the expense of some performance. Each web application has its own process
space.
For average hit rate sites, the IIS5 default is probably the best. Changing
a web app's level to "low" will give noticeable improvements on high hit
rate sites.
DISCLAIMER: Note these pages are a free resource for anyone wishing to reference them. Although every care is taken to ensure their correctness, the author takes no responsibility for any errors or problems that may occur through their use, or indeed misuse. These pages are copyight of Dave Clarke, Visualize Software Ltd 1997-2000 (all rights reserved).
© Copyright
Dave Clarke, 1996-2010
|
|