As the web continues to evolve, the popularity of little widgets to display on your web pages -- Facebook "Like" buttons, "Tweet This" buttons, and the like -- are growing more popular. Like it or not, you may be asked to implement one (or a dozen) of them one day. This introduces potential points of failure, as each external resource could potentially go offline at any time for any duration. Nothing drives home the idea that nobody is immune more than the fact that the behemoth social network Facebook has unexpectedly gone offline twice in the last two weeks. How do you protect your websites from additional stress in the form of delayed page loading when an event like this occurs?
With careful planning.
The first thing we need to do is understand the problem. I will use the Facebook "Like" button because I'm familiar with it, but the concept applies to nearly anything you include directly from another website.
The problem
When you include a <script> block in the middle of your <body> block, the browser will stop rendering the DOM at that point, download the script file, execute it, and then continue. This is referred to as Blocking (of page rendering), and is the most severe example. This is why conventional wisdom says constantly reiterates that you should place <script> blocks as the very last thing inside the <body> block -- so that if the script source is unavailable or slow, the rest of your page is not waiting on it.
Facebook "Like" buttons, fortunately, don't require a <script> block to embed themselves on your pages. Instead, they use <script>'s slightly less evil cousin, iFrame. IFrames do not block page rendering; which is to say that they are rendered in parallel with everything else on the page. However, they still present two problems. First, they share their connection pool with the parent page, and second, and probably more importantly, they do block the page's onLoad event.
The onLoad event is what a majority of JavaScript frameworks recommend waiting for to initialize your applications (rightfully so). What this means is that, if Facebook were to go down, and you have 1 "Like" button on the page, the browser's downloading/busy indicator won't stop until the request to Facebook for the iFrame content times out -- the duration of which depends on the user's browser -- and then, and only then, will the onLoad event fire. If you are unlucky enough to have multiple "Like" buttons on a page, then they each must timeout before onLoad will fire. Depending on the number of buttons you're using, they could either all be executing in parallel, or if you've got more buttons than threads in the connection pool, then you'll have some queued up, effectively doubling (or more!) the already seemingly-eternal wait.
If your website isn't of much use without the JavaScript that runs when the onLoad event fires, then chances are good that users aren't going to wait around for 30 seconds to a minute. I know I wouldn't -- I give up on even the most interesting link after what feels like 5 seconds.
The solution
Not to fret, there is a simple and elegant way to add Facebook "Like" buttons (and their ilk) after the page has completed loading, which will provide your users with the best experience, especially when the external sites you depend on end up on a milk carton.
My suggestion is to start with a placeholder that contains all of the information you need to build the iFrame.
<a class="fbLike" href="{iFrame url}"></a>
If, instead of adding an iFrame, you use the same URL in an Anchor (<a></a>) tag, then the page will continue to load as normal, and the user will see nothing. I've added a class to make identifying all of the placeholders for my FB Like buttons simple.
Now, after the page has loaded and the onLoad event fires, we want to replace our placeholders with iFrames to inject the buttons. I'll do this with a few lines of jQuery:
$(function(){
$(".fbLike").each(function(){
var t = $(this);
var info = t.attr("href");
t.replaceWith("<iframe src='" + info + "'></iframe>");
});
});
Note that I've left off some of the iFrame tag attributes for the sake of keeping the code sample short and sweet; just add them back as needed.
What this does is iterate over every placeholder on the page and replace it with an iFrame, using the same URL that the placeholder anchor tag had been using. I'm not sure if this will restart the browser's busy indicator, but at least the rest of your website will be usable.
Bonus
Since you're setting a placeholder, one nice usability improvement would be to style that placeholder to indicate to users that your FB Like button is loading. Perhaps give it a background image of the Like button grayed out, and a tooltip of "Loading Like button from Facebook..."
Posted in
Best Practices |
Facebook |
JavaScript |
jQuery
| 8 Responses
October 06 2010
These days, I work with CF servers that are home to hundreds of applications simultaneously. We have some nice error reporting going on, with Application.cfc's onError() method to send email notifications, and using a backup of the CFError tags for when all else has failed. But I noticed that it was incredibly annoying waiting for error emails to arrive when my code would have an error in it. So I decided to disable custom error handling and pretty error pages in development.
Of course, to eliminate the potential for human error, we programatically determine if the application is running in dev, staging, or production:
<cfinvoke
component="cfc.AppStatus"
method="getEnvName"
returnVariable="appStatus"
/>
This code is in Application.cfc, and stores the result in a place shared with the entire application. We were already using this value to set things like the datasource password (which is different between your development and production environments, right?!). If it's not obvious, the function will return the string DEV, STAGE or PROD depending on which environment that server belongs to.
Then, in onRequestStart(), I added this little nugget:
<!--- If on dev, don't use error emails, just show errors --->
<cfif app_status neq "prod" and app_status neq "stage">
<cfset structDelete(this, "onError") />
<cfset structDelete(variables, "onError") />
<cfsetting showdebugoutput="true" />
</cfif>
And wrap the backup CFError tags like so:
<cfif app_status eq "prod" or app_status eq "stage">
<cferror
type="request"
template="/error_request.cfm"
/>
<cferror
type="exception"
template="/error_exception.cfm"
/>
</cfif>
Now, when my code throws an error while I'm developing and testing in the dev environment, I see it on screen instead of having to wait a minute or two for the email to come, and I don't have to worry about a rogue infinite loop filling up my inbox.
Posted in
Best Practices |
ColdFusion
| 2 Responses
May 11 2010
This afternoon I have been having a bit of a group discussion on the perils of INNER JOINs in SQL. It started with a tweet that Inner Join's are dangerous and you should use them with caution. A bunch of people asked me what exactly I meant, and of course it's difficult to get that across in chunks of 140 characters or less, which brings us to the present and this blog post.
Consider that you have a relational database setup to store survey responses. Each respondent creates 1 ResponseSet, which gets a ResponseSetId, and then from there you can build up responses to the various questions. There are N questions. Each question can have M responses, all of them free-form text. There are probably a hundred ways to lay out the database to support this, but in the case of the database that inspired the tweet, I've got a Response table, with a foreign key to the ResponseSet table, which has a foreign key to the Survey table. A response belongs to a response set, which belongs to a survey.
In your mind, write some SQL to join these tables and get back the questions and answers for a given ResponseSet for a report (say, ResponseSetId 473240432, if that helps you think). Here is the first thing that pops into my head:
SELECT
QuestionId
, ResponseText
FROM
tblResponseSet rs
INNER JOIN tblResponse r ON rs.ResponseSetId = r.ResponseSetId
INNER JOIN tblQuestion q ON q.QuestionId = r.QuestionId
WHERE
rs.ResponseSetId = @responseSetId
GROUP BY
q.QuestionId
ORDER BY
q.QuestionSort
That's great, right? Unfortunately, no. At least, not in my case. Even assuming that all questions are mandatory (1+ responses required), this does not account for other buggy software I've written (we all do it!) that saves, copies, or modifies the data that may result in blank responses or no responses for a given question. Rather to the point, what I would rather see in the case of this poorly-maintained theoretical data (as modified by my buggy code) is the rows I expect, but with NULL values for missing responses.
In the event of the survey above, it would be more helpful to the developer trying to debug the application to know that NULLs are coming back as survey responses, than it would be to know that the question isn't being included in the result set. The join for the question data is fine -- at least in as much as it isn't the cause of this theoretical row being dropped -- it's the join on responses that's causing the problem. In effect, by eliminating the row altogether, we're creating a false-negative, or the appearance of a bug that doesn't exist. (The question table should be joined via outer join as well.)
Using this method of querying the data, each row in the result set will give the question and 1 of its responses. The question will repeat with each different response, making it easy to use the group attribute of ColdFusion's cfoutput tag to display each question with the various responses to it for the given Response Set. However, with the above query, if there are no responses for ResponseSetId 473240432 and QuestionId 42, then QuestionId 42 is dropped from the result set. That's a Bad Thing (TM)!
In essence, plan for the worst but hope for the best. Whenever I write a join condition, instead of asking myself "How is this data relationship supposed to work?" I should be asking myself "What do I want to happen if some data turns up missing?" By writing an inner join, I'm answering the second question with "drop the row". The answer I wanted to provide for that question was to "show nulls", which is written in SQL with an OUTER JOIN.
That is the inherant danger of INNER JOINs of which I was speaking.
Posted in
Best Practices |
Databases
| 4 Responses
February 02 2010
I'm giving a presentation at the end of the month on Subversion for my office. It's going to be recorded and I'll be sure to post the video here for anyone interested in watching.
Here's where I need your help: What are some of the craziest, dumbest, most ridiculous things you've ever seen done in Subversion (or some other version control system, as long as the scenario would still apply)?
Anything is fair game. Awful commit comments? Terrible branching or merging practices? I want to hear about it all, and I welcome your comments!
Posted in
Best Practices |
Subversion
| 1 Response
May 11 2009