Goal
To improve the Google PageSpeed scores of this site.
Background
I use MathJax and fancyBox for displaying math equations and images respectively.
Problem
As shown in the following two pictures, the PageSpeed scores of the homepage of this blog for “Mobile” and “Desktop” were very low.
Solution
I followed the advice of Google Developers, and deferred loading of JavaScripts.
Results
After a week’s work, the PageSpeed has risen.
HTML Headers
I removed render-blocking JavaScripts and stylesheets in the HTML Headers.
- Deleted all
<script>
and<link rel="stylesheet" ... />
tags insource/_includes/custom/head.html
, except<link src="/stylesheets/print.css" ... />
. - Deleted the lines which loads jQuery, Modernizr,
/javascripts/octopress.js
and/stylesheets/screen.css
.
1 2 3 4 5 6 7 8 9 |
|
Combine and compress fancyBox JavaScripts
I used the online JavaScript compressor at http://jscompress.com. First, I clicked the “Upload Javascript Files” tabbed pane. After that, uploaded the three JavaScripts for fancyBox helpers:1
/fancybox/source/helpers/jquery.fancybox-buttons.js?v=1.0.5
/fancybox/source/helpers/jquery.fancybox-media.js?v=1.0.6
/fancybox/source/helpers/jquery.fancybox-thumbs.js?v=1.0.7
I then downloaded the compressed output.
After the HTML footer
To load the scripts, especially Google hosted jQuery (or a local copy
of it in case that the remote library can’t load) and fancyBox,
and the stylesheets after the contents in the <body>
tag has been
loaded, I added the following lines to
source/_includes/custom/after_footer.html
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
|
Improve user experience
Google PageSpeed Insights said that my homepage was too wide for
mobile devices whose width were 320 px. Therefore, I’ve tried
preparing different pictures for windows of different width by
<picutre>
tags with <source srcset=...>
inside. Although this
pure HTML 5 approach is now supported in Google Chrome only, I find
this method simple and beautiful.2 Due to its elegance, I
believe that this feature will soon be implemented in all major
browsers, such as Mozilla Firefox.
1 2 3 4 5 |
|
The simplest way to avoid getting the message “Optimize images” in a PageSpeed test is to optimize the images. OptiPNG can be used for this. Before compressing the images, I issued the following command to create a diminished image of width 300 px of the original image.
$ optipng big.png
$ convert big.png -resize 300 small.png
$ optipng small.png
How to enable fancyBox for <picture>
elements? I did a bit of
“guided coding” — I copied Erv Walter’s function and changed a few
lines.3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
View the file at commit 29d86dd for a working example.
Lastly, to ensure that the contents wouldn’t fall outside the viewport on the screen of mobile devices, I embedded an inline CSS.
1
|
|
Though this wasn’t good, it enabled me to get rid of the above message about image optimization in a PageSpeed test.
Lessons learnt
My first way to load JavaScripts from a script
Due to my homework and exams in early December and my limited
knowledge on JavaScripts, I didn’t have time to understand the
section “Defer loading of JavaScript” in
a suggestion page on Google Developers. On Dec 20,
2014, I found an article on feedthebot useful.4 I copied
the codeblock and changed defer.js
to other files.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
In M$ I.E., one has to use window.attachEvent
instead of
window.addEventListener
. The last line acts as a fallback for old
browsers.
This works for MathJax. However, for fancyBox, which depends on jQuery, strange things happened, and I got trapped for two days. I had wrote a function which worked in Google Chrome, but when I opened Mozilla Firefox to test the results, no box popped out and I was taken to a page containing merely the picture. I was puzzled and made a Git commit.5
My second way to load JavaScripts from a script
I googled “defer jquery fancybox”, and found a question on Stack Overflow.6 I made use of Jamed Donnelly’s functions, and had successfully made a popped up image, which meant that the loading of JavaScripts for fancyBox had been deferred.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 |
|
Nonetheless, the image title and the close icon were missing.
View loaded resources and the timeline
I had tried using “Network” in the developer toolbar in both Google
Chromium and Mozilla Firefox to view the time taken for loading the
scripts and CSS files. I contrasted the timelines of working sites
and my failed trials, and then made changes to the above script before
the </body>
tag.
Test Javascript functions using the web console
While testing the script in
source/_includes/custom/after_footer.html
, the scripts often
failed to work. The error console in web browsers spotted out the
missing brackets and other syntax errors. Moreover, it told me
whether jQuery had been loaded if I typed typeof jQuery
.
Access the children of an HTML element with jQuery
While writing the anonymous function in
/javascripts/FancyBoxLocal.js
, I tried to extract the URL of the
<source>
element inside a <picture>
element. In the web console,
if typeof jQuery
returns "function"
,
$('picture').children('source').length
returns a non-negative
integer. If the integer returned is positive,
$('picture').children('source')[0].srcset
returns
"/images/foo.png"
and "undefined"
in Google Chromium and Mozilla
Firefox respectively.
I played with the example on W3Schools about the children()
method
in jQuery.7 After re-writing the code and seeing the result
repeatedly, I’ve learnt to use the eq(n)
and attr(source)
methods
to access the n
-th element and the source
attribute.
Load CSS files from a script
Google Developers has already provided a script for doing
so.8 At first, I didn’t understand it and I tried
injecting a <link>
element inside the downloadJSAtOnload()
method.
Unluckily, this method failed. Without /stylesheets/screen.css
,
the Solarized theme disappeared, and was “replaced by a
black-and-white theme”. After that, I re-read Google’s page on CSS
delivery optimization, and extracted the script which grabbed external
stylesheets.
1 2 3 4 5 6 7 8 9 10 11 |
|
From “Network” in the web developer toolbar, in an up-to-date version
of modern browsers, this anonymous function which loads external CSS
files should be executed before script.onload
and window.onload
.
In addition, unlike Javascripts, I don’t use something like
stylesheet.onload
. Simply put the relative path or URL of the
external CSS files inside the anonymous function will do.
After I grabbed the stylesheets before the external Javascripts
loaded, the missing components of the pop up window were still
missing. It took me half an hour to figure out that l.rel =
'stylesheet'
couldn’t be omitted. I then pushed commit 1d40cea
to GitHub and claimed that I’d found a way for both Google Chrome and
Mozilla Firefox.
The next day, I tested it with M$ I.E., and older versions of Google
Chrome and Mozilla Firefox. In older versions, my configurations had
failed, but they worked fine in the recent ones. For example, the
method of loading external Javascripts in a non-render-blocking manner
as suggested by Google Developers didn’t work in Mozilla Firefox 14,
which didn’t support requestAnimationFrame
. Though
mozRequestAnimationFrame
was supported, if they’re put together, the
whole thing would collapse. Since I’m not a web
designer/programmer, I won’t consider fixing this problem since
- the old versions of browsers will be eventually upgraded, and more importantly,
- I’ve other (non-technical) problems to solve.
Onload and createElement()
I’ve read a question on Stack Overflow, and understood that even
though a <script>
element with the src='/javascripts/foo.js'
attribute had been created by the createElement()
function, the
functions defined in the external script /javascripts/foo.js
could
be unusable if one doesn’t make proper use of script.onload
.
A fallback for Google hosted jQuery
(Added on AUG 3RD, 2015)
Unluckily, this failed on mobile devices. I was told that I
should have this problem fixed. Due to the difficulty described
below and the enormous number of mobile device users, I decided to
remove the part of JavaScript source code which was responsible for
loading a local copy of jQuery. This would reduce over 9,600 lines
of code in the master
branch.
In my opinion, this is the most difficult part, which has taken me a whole day.
I observed that loading both the local and remote copy of jQuery was incorrect. Since jQuery is relatively large, leaving this problem unsolved is like leaving a black dot on a whiteboard.
One can’t simply remove the local copy of jQuery since Google is blocked in some regions.9 There’re some political reasons behind. Unlike Zigang Xiao’s blog, I’m not going to write any articles on politics.10 I’ll just seek a technical solution to this political problem.
This problem is extremely difficult for me since I’m not a student
of IT, and there’s no direct solution from the Internet. Inside and
outside the function triggered by an onload
event, code aren’t
executed sequencially, but in parallel. Therefore, if I put if
(typeof jQuery == 'undefined')
after getScript(...)
, then even
though the Google hosted jQuery library can be downloaded, the if
statement returns true. Google Chromium and Mozilla Firefox showed
different results for the same JavaScript. This increased the
difficulty of the task.
While coming up with a solution, I looked at
source/_includes/head.html
, and found the following line.
1 2 |
|
Not being familiar with JavaScript syntax, I searched for the
meaning of the unescape()
function and that of the ‘.’ in front of
/javascripts/...
. After viewing a question on Stack Overflow, I
decided to create the pull request #1699 of Octopress.
Luckily, it was accepted and merged into the master
branch of
Octopress.
After doing some off-topic stuff, I curiously reproduced the error described in the first subsection of this section, and figured out a way to fix this error. During the process, I’ve learnt the dependence of difference scripts Therefore, the correct sequence should be
- Load
jquery.min.js
. - Load
octopress.js
and/fancybox/source/jquery.fancybox.js
. - Load
/fancybox/source/helpers/jquery.fancybox-*.js
. - Load
FancyBoxLocal.js
.
For example, appendChild(octopress)
should be inside
jquery.onload = function() {...}
.
Finally, using “Network” in the web developer toolbar in Google
Chromium, I observed that before the window.onload
event, the
getScript(...)
method had already got the scripts. I combined the
two ways, and solved the problem.
-
I didn’t use the mousewheel plugin. If one uses a mouse click to get the popped up window, then he/she can also view the next/previous picture with a click. ↩
-
See, for example, Built-in Browser Support for Responsive Images by Pearl Chen on HTML 5 Rocks, for an introduction to the
<picture>
element. ↩ -
See Octopress Customizations by Erv Walter on ewal.net. ↩
-
Defer Loading Javascript by Patrick Sexton on feedthebot. ↩
-
See How to defer javascript on Stack Overflow for the function. ↩
-
The online example is available at http://www.w3schools.com/jquery/tryit.asp?filename=tryjquery_children2 ↩
-
See Optimize CSS delivery on Google Developers for details. ↩
-
Google Is Blocked In 25 Of The 100 Countries They Offer Products In by MG Siegler on TechCrunch. ↩