My goal was to remove all external JavaScript files from the page and still be modular. After a year of procrastinating I finally got to it and development on the website has never been easier and I didn't sacrifice speed or modularity.
Not long ago, I tried to test my website on Google Page speed and it showed me that one of the things slowing down my website was CSS and JavaScript blocking the rendering of the page. Despite the fact that they are called web pages, most websites are dynamically generated like an application. I use PHP in my case. By using good programming paradigms and being organized, I managed to turn all my external CSS into a single inline CSS per page. Now, I thought it's time to do the same for JavaScript.
Why use External JS?
Just like CSS, making your javascript external helps separate the logic from presentation. It also makes it easier for different developers to work on the website each with their own role.
In general, it is a good idea to use an external JavaScript file, but when your website is dynamic, and is programmatically generated, it is possible to inject javascript directly on the page.
Why I wanted to ditch External JavaScript?
Premature Optimization is the root of all evil. But too many requests on your page is also a problem. Having blocking scripts on the page may result making your website to appear much slower.
JavaScript presented a little more challenges because unlike CSS it is a programming language. The order in which I load my scripts is crucial. I couldn’t simply slap all my scripts on the page because it would cause errors. My goal was to dynamically add scripts on my page depending on which feature is currently being used.
The first step was to convert my old scripts to run asynchrounously. This is a challenge, especially if you already have a working website. It means going through all your scripts and updating them to work with the new functionality.
You can read previous tutorials where I showed how to add all you JavaScript asynchrounously without breaking your website:
First take a look at the page source of this page. Scroll all the way to the bottom and you will see a minified bunch of scripts. This would be very hard to debug but luckily it is only like this when I deploy it on my production server.
In reality, it is multiple scripts that are combined together based on the page you are visiting. For example, on a blog post page I have:
- Utility library script
- search function
- syntax highlighting
- commenting system.
Each of those are different modules on my page, and here is how I load them.
Articles:
<?php
$view->addJS("Blog:Main:syntaxHighlight.js"); // syntax highlighting script
?>
<article itemscope itemtype="http://schema.org/Article">
<header <?php echo $headDiv?>>
<div class="article__header">
<div class="article__title">
...
<script>_simply.push(['trigger','syntaxHighlight']);</script>
Comments:
<?php
$view->addJS("Blog:Main:comment.js");
?>
<div class="comment-section" id="comment-section">
<h3 class="comment-section__title">Comments</h3>
...
<script>_simply.push(['trigger','comment',{postId:10}]);</script>
By using the method addJS()
I am keeping track of which script is to be loaded on this particular page. At the bottom of the page I simply print it using a method called printJS()
<?php echo $footer;?>
<?php echo $view->printJs();?>
</body>
</html>
And I use the asynchronous function caller _simply.push(['trigger','...'])
to activate each of the features.
Internally this is how the code works:
class JsManager {
private static $jsFiles = array();
public static function addJS($jsNamespace){
if(isset(self::$jsFiles[$jsNamespace])){
return;
}
self::$jsFiles[$jsNamespace] = true;
}
public static function printJS($return = false){
switch (ENVIRONMENT) {
case "prod":
$signature = md5(implode("", array_keys(self::$jsFiles)));
$memcache = new \MemCache();
$value = $memcache->get($signature);
if ($value !== false){
$result = $value;
break;
}
$result = "<script>".self::implodeJS(true)."</script>";
$memcache->set($signature,$result,self::CACHE_ONE_DAY);
break;
default:
$result = "<script>".self::implodeJS(false)."</script>";
break;
}
if ($return){
return $result;
}
echo $result;
}
private static function implodeJS($minify = false){
$jsContent = [];
foreach(self::$jsFiles as $namespace =>$val){
if ($minify){
$jsContent[] = Minifier::minify($namespace);
}else {
$jsContent[] = file_get_contents($namespace);
}
}
return implode("", $jsContent);
}
}
addJs()
This method allows us to specify which JavaScript file to use. Note that it makes sure that the same file is not added multiple times.
implodeJs()
This method runs through all the js files set using addJs and combines them into a single string. Note that if the parameter $minify
is set to true, the string is also minified. The minification is done using a third party library called JShrink. You can find it on github.
printJS()
This method is called at the end, after all the scripts where added. It prints the combined (and minified) string on the page.
And just like that we have can add JavaScript inline on the page without worrying about having problem maintaining it. Here is an example of a script on the page:
(function(){
function search(keyword){
keyword = keyword.replace(/([^a-zA-Z0-9\s])/g,"").replace("/^(\s+)/","").replace("/(\s+)$/","");
keyword = keyword.replace(/(\s+)/g," ");
var all = document.getElementsByTagName("*"),
// validClasses = ["search__snippet-text","search__item-title"],
validClasses = ["search__snippet-text","js-search__item-title"],
elems = [],
keys = keyword.split(" "),
regex = new RegExp("\\b("+keys.join("|")+")(?=[^\w])","gi");
for(var i =0,l = all.length;i<l;i++){
for (var j=0,k=validClasses.length;j<k;j++){
if (all[i].className.indexOf(validClasses[j]) !== -1){
elems.push(all[i]);
}
}
}
for (var i = 0,l = elems.length;i<l;i++){
elems[i].innerHTML = elems[i].innerHTML.replace(regex,"<b>$1</b>");
}
}
_simply.push(["register","search",search]);
})();
This script is added to the bottom of the page, a function "search" is registered as an async function. When the search module is added the function call "search" is triggered using this method _simply.push(['trigger','search','foo bar'])
The code is modular and each file is saved separately and easy to update.
In case you need to see how I made JavaScript functions Asynchronous I added my Utility file below:
Enjoy!
Comments
There are no comments added yet.
Let's hear your thoughts