Connect with us

Tech

Create a Node.js tool to save and compare Google Lighthouse reports

 


In this tutorial, I'm going to show you step by step how to create a simple tool in Node.js to run Google Lighthouse audits via the command line, save the reports they generate in JSON format then compare them so that the Web performance can be monitored as the website grows and develops.

I hope this can be a good introduction for any developer who wants to learn to work with Google Lighthouse programmatically.

But first, for the uninitiated …

What is Google Lighthouse?

Google Lighthouse is one of the best automated tools available on a web developer's utility belt. It allows you to quickly audit a website in a number of key areas which together can be a measure of its overall quality. These are:

  • Performance
  • Accessibility
  • Best practices
  • SEO
  • Progressive Web App

Once the audit is complete, a report is then generated on what your website is doing well and not well, the latter intending to serve as an indicator for what your next steps should be for improve the page.

Here's what a full report looks like.

Along with other general diagnostics and web performance measures, a very useful feature of the report is that each of the key areas is aggregated by color-coded scores between 0 and 100.

Not only does this allow developers to quickly assess the quality of a website without further analysis, but it also allows non-technical people such as stakeholders or customers to understand.

For example, that means it's much easier to share the marketing win with Heather after spending time improving website accessibility, as she is more able to appreciate it. # 39; effort after seeing the accessibility score of the lighthouse increase by 50 points in the green.

But in the same way, Simon, the project manager, may not understand what Speed ​​Index or First Contentful Paint means, but when he sees the Lighthouse report showing the website performance score at knees in the red, he knows you still have some work to do.

If you're in Chrome or the latest version of Edge, you can run a Lighthouse audit by yourself right now using DevTools. Here's how:

You can also perform a flagship audit online via PageSpeed ​​Insights or via popular performance tools, such as WebPageTest.

However, today only Lighthouse was a Node module, as this allows us to use the tool programmatically to audit, record and compare web performance measures.

Let's see how.

Install

First of all, if you don't have it already, you'll need Node.js. There are a million different ways to install it. I use the Homebrew package manager, but you can also download an installer directly from the Node.js website if you prefer. This tutorial was written with Node.js v10.17.0 in mind, but will most likely work fine on most versions released in recent years.

You will also need Chrome installed, as this is how well Lighthouse audits are performed.

Then create a new directory for the project, then cd in it in the console. Then run npm init to start creating a package.json file. At this point, I just recommend hitting the Enter key over and over to ignore as much as possible until the file is created.

Now create a new file in the project directory. I called mine lh.js, but feel free to call it whatever you want. This will contain all of the tool's JavaScript. Open it in the text editor of your choice, and for now, write a console.log declaration.

console.log('Hello world');

Next, in the console, make sure your CWD (current working directory) is your project directory and run node lh.js, replacing my filename with everything you used.

You should see:

$ node lh.js
Hello world

Otherwise, check that your Node installation is working and that you are definitely in the right project directory.

Now, this is out of the way, we can move on to the development of the tool itself.

Opening Chrome with Node.js

Allows to install our projects in first dependence: the lighthouse itself.

npm install lighthouse --save-dev

This creates a node_modules directory containing all the package files. If you're using Git, the only thing you'll want to do with it is add it to your .gitignore file.

In lh.js, you will then want to delete the test console.log() and import the Lighthouse module so you can use it in your code. So:

const lighthouse = require('lighthouse');

Below, you will also need to import a module called chrome launcher, which is one of Lighthouses' dependencies and allows Node to launch Chrome on its own so that the audit can be run.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

Now that we have access to these two modules, let's create a simple script that simply opens Chrome, performs a Lighthouse audit, and then prints the report to the console.

Create a new function that accepts a URL as a parameter. Because doing this well using Node.js, we were able to safely use the ES6 syntax because we don't have to worry about Internet Explorer users.

const launchChrome = (url) => {

}

In function, the first thing we need to do is to open Chrome using the chrome launcher that we imported and send it to any argument passed by the url setting.

We can do this using its launch() method and sound startingUrl option.

const launchChrome = url => {
  chromeLauncher.launch({
    startingUrl: url
  });
};

Calling the function below and passing a URL of your choice will cause Chrome to open the URL when running the Node script.

launchChrome('https://www.lukeharrison.dev');

The launch function actually returns a promise, which allows us to access an object containing some useful methods and properties.

For example, using the code below, we can open Chrome, print the object on the console, and then close Chrome three seconds later using its kill() method.

const launchChrome = url => {
  chromeLauncher
    .launch({
      startingUrl: url
    })
    .then(chrome => {
      console.log(chrome);
      setTimeout(() => chrome.kill(), 3000);
    });
};

launchChrome("https://www.lukeharrison.dev");

Now that we understand Chrome, let's move on to the lighthouse.

Programming Lighthouse

First, let's rename our launchChrome() function to something more reflecting its final functionality: launchChromeAndRunLighthouse(). With the difficult part out of the way, we can now use the Lighthouse module that we imported earlier in the tutorial.

In Chrome launchers, then the function, which does not run until the browser is open, passes the functions to the flagship url argument and trigger an audit of this site.

const launchChromeAndRunLighthouse = url => {
  chromeLauncher
    .launch({
      startingUrl: url
    })
    .then(chrome => {
      const opts = {
        port: chrome.port
      };
      lighthouse(url, opts);
    });
};

launchChromeAndRunLighthouse("https://www.lukeharrison.dev");

To link the flagship instance to our Chrome browser window, we need to pass its port with the URL.

If you were to run this script now, you will get an error in the console:

(node:47714) UnhandledPromiseRejectionWarning: Error: You probably have multiple tabs open to the same origin.

To solve this problem, we just need to delete the startingUrl from Chrome Launcher and let Lighthouse manage URL navigation from here.

const launchChromeAndRunLighthouse = url => {
  chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    lighthouse(url, opts);
  });
};

If you were to run this code, you will notice that something definitely seems to be happening. We just haven't received any comments in the console to confirm that the Lighthouse audit has worked well, and the Chrome instance does not close by itself as before.

Fortunately, the lighthouse() The function returns a promise that allows us to access the audit results.

Allows you to kill Chrome and then print these results on the terminal in JSON format via the report property of the results object.

const launchChromeAndRunLighthouse = url => {
  chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    lighthouse(url, opts).then(results => {
      chrome.kill();
      console.log(results.report);
    });
  });
};

Although the console is not the best way to display these results, if you copy them to your clipboard and visit the Lighthouse Reports Viewer, pasting here will show the report in all its splendor.

At this point, it is important to tidy up the code a bit so that the launchChromeAndRunLighthouse() returns the report when it finishes executing. This allows us to process the report later without causing a messy JavaScript pyramid.

const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");

const launchChromeAndRunLighthouse = url => {
  return chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    return lighthouse(url, opts).then(results => {
      return chrome.kill().then(() => results.report);
    });
  });
};

launchChromeAndRunLighthouse("https://www.lukeharrison.dev").then(results => {
  console.log(results);
});

One thing you may have noticed is that our tool can currently only audit one website. Lets change this so that you can pass the URL as an argument via the command line.

To avoid working with command line arguments, manage them well with a package called yargs.

npm install --save-dev yargs

Then import it at the top of your script with Chrome Launcher and Lighthouse. We only need her argv operate here.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;

This means that if you were to pass a command line argument to the terminal like this:

node lh.js --url https://www.google.co.uk

… you can access the argument in the script like this:

const url = argv.url // https://www.google.co.uk

Lets edit our script to pass the command line URL argument to functions url setting. It is important to add a little safety net via the if instruction and error message if no argument is passed.

if (argv.url) {
  launchChromeAndRunLighthouse(argv.url).then(results => {
    console.log(results);
  });
} else {
  throw "You haven't passed a URL to Lighthouse";
}

Tada! We have a tool that launches Chrome and performs a Lighthouse audit programmatically before printing the report on the terminal in JSON format.

Saving lighthouse reports

Printing the report on the console is not very useful since you cannot easily read its content, nor are they saved for future use. In this section of the tutorial, modify this behavior so that each report is saved in its own JSON file.

To prevent reports from different websites from getting mixed up, organize them like this:

  • lukeharrison.dev
    • 2020-01-31T18: 18: 12.648Z.json
    • 2020-01-31T19: 10: 24.110Z.json
  • cnn.com
    • 2020-01-14T22: 15: 10.396Z.json
  • lh.js

Be sure to name the reports with a timestamp indicating when the date / time the report was generated. This means that no report filename will ever be the same, and this will help us to easily distinguish reports.

There is a problem with Windows that requires our attention: the colon (:) is illegal for file names. To alleviate this problem, replace all colons with underscores (_), so a typical report file name will look like:

  • 2020-01-31T18_18_12.648Z.json

Creation of the directory

First of all, we need to manipulate the command line URL argument so that we can use it for the directory name.

This involves more than simply removing the www, because it must take into account audits performed on web pages that are not at the root (for example: www.foo.com/bar), because forward slashes are invalid characters for directory names.

For these URLs, replace the invalid characters with underscores. That way if you run an audit on https://www.foo.com/bar, the name of the resulting directory containing the report would be foo.com_bar.

To facilitate URL processing, use a native Node.js module called url. It can be imported like any other package and without having to add it to thepackage.json and pull it out via npm.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');

Then lets use it to instantiate a new URL object.

if (argv.url) {
  const urlObj = new URL(argv.url);

  launchChromeAndRunLighthouse(argv.url).then(results => {
    console.log(results);
  });
}

If you were to print urlObj at the console you will see a lot of useful URL data that we can use.

$ node lh.js --url https://www.foo.com/bar
URL {
  href: 'https://www.foo.com/bar',
  origin: 'https://www.foo.com',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'www.foo.com',
  hostname: 'www.foo.com',
  port: '',
  pathname: '/bar',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}

Create a new variable called dirNameand use the string replace() method on the host property of our url to get rid of the www in addition to https protocol:

const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace('www.','');

Weve used let here, which unlike const can be reassigned, as well as update the reference if the URL has a path name, to replace the slashes with underscores. This can be done with a regular expression pattern and looks like this:

const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "https://css-tricks.com/") {
  dirName = dirName + urlObj.pathname.replace(///g, "_");
}

We can now create the directory itself. This can be done using another native Node.js module called fs (abbreviation for "file system").

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
const fs = require('fs');

We can use her mkdir() to create a directory, but must first use its existsSync() to check if the directory already exists, because Node.js would otherwise generate an error:

const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "https://css-tricks.com/") {
  dirName = dirName + urlObj.pathname.replace(///g, "_");
}
if (!fs.existsSync(dirName)) {
  fs.mkdirSync(dirName);
}

Testing the script at this point should result in the creation of a new directory. Who passed https://www.bbc.co.uk/news because the URL argument would result in a directory named bbc.co.uk_news.

Save the report

in the then function for launchChromeAndRunLighthouse(), we want to replace the existing console.log with logic to write the report to disk. This can be done using the fs modules writeFile() method.

launchChromeAndRunLighthouse(argv.url).then(results => {
  fs.writeFile("report.json", results, err => {
    if (err) throw err;
  });
});

The first parameter represents the name of the file, the second is the content of the file and the third is a callback containing an error object if there is a problem during the writing process. This would create a new file called report.json containing the JSON object of the returned Lighthouse report.

We always need to send it to the correct directory, with a timestamp as the file name. The first one is simple. dirName variable that we created earlier, like this:

launchChromeAndRunLighthouse(argv.url).then(results => {
  fs.writeFile(`${dirName}/report.json`, results, err => {
    if (err) throw err;
  });
});

The latter however obliges us to somehow retrieve a timestamp of the generation of the report. Fortunately, the report itself captures this as a data point and is stored as fetchTime property.

We just need to remember to swap the colon (:) for underscores (_) so it plays well with the Windows file system.

launchChromeAndRunLighthouse(argv.url).then(results => {
  fs.writeFile(
    `${dirName}/${results("fetchTime").replace(/:/g, "_")}.json`,
    results,
    err => {
      if (err) throw err;
    }
  );
});

If you were to run it now, rather than one timestamped.json filename, you will probably see an error similar to:

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined

This happens because Lighthouse is currently returning the report in JSON format, rather than an object consumable by JavaScript.

Fortunately, instead of parsing the JSON ourselves, we can just ask Lighthouse to return the report as a normal JavaScript object instead.

This requires modifying the line below from:

return chrome.kill().then(() => results.report);

…at:

return chrome.kill().then(() => results.lhr);

Now if you run the script again, the file will be named correctly. However, once opened, its only content will unfortunately be …

(object Object)

It is because we now have the opposite problem as before. I was trying to render a JavaScript object without first filtering it into a JSON object.

The solution is simple. To avoid having to waste resources by analyzing or stratifying this huge object, we can return both lighthouse types:

return lighthouse(url, opts).then(results => {
  return chrome.kill().then(() => {
    return {
      js: results.lhr,
      json: results.report
    };
  });
});

Then we can change the writeFile for example to this:

fs.writeFile(
  `${dirName}/${results.js("fetchTime").replace(/:/g, "_")}.json`,
  results.json,
  err => {
    if (err) throw err;
  }
);

Sorted! At the end of the flagship audit, our tool should now save the report to a file with a unique timestamped filename in a directory named after the website URL.

This means that reports are now organized much more efficiently and will not replace each other, regardless of the number of reports saved.

Comparison of Lighthouse Reports

During daily development, when I'm focused on improving performance, the ability to quickly compare reports directly in the console and see if I'm heading in the right direction could be extremely helpful . With this in mind, the requirements for this comparison functionality should be:

  1. If a previous report already exists for the same website when a Lighthouse audit is completed, automatically compare to it and view changes made to key performance measures.
  2. I should also be able to compare the key performance metrics from two reports from two websites without having to generate a new Lighthouse report that I might not need.

Which parts of a report should be compared? These are digital key performance indicators collected as part of any Lighthouse report. They provide insight into the objective and perceived performance of a website.

In addition, Lighthouse also collects other metrics which are not listed in this part of the report, but which are always in an appropriate format to be included in the comparison. These are:

  • First byte time – Time To First Byte identifies the time when your server sends a response.
  • Total blocking time – Sum of all periods between FCP and Time to Interactive, when the task duration exceeds 50 ms, expressed in milliseconds.
  • Estimated input latency – Estimated input latency is an estimate of the time it takes your application to respond to user input, in milliseconds, during the busiest 5-second page load window. If your latency is greater than 50 ms, users may perceive your application as off-beat.

How should the metric comparison be sent to the console? Create a simple comparison based on percentages using the old and new measures to see how they have changed from one ratio to another.

To allow quick scanning, you can also color code individual measurements according to whether they are faster, slower, or unchanged.

Aim for this exit:

First Contentful Paint is 0.49% slower
First significant paint is 0.47% slower
The speed index is 12.92% slower
The estimated input latency is the same
85.71% faster total lockup time
The maximum potential entry delay is 10.53% faster
19.89% slower delay before first byte
First idle processor is 0.47% slower
The interactivity time is 0.02% slower

Compare the new report with the previous report

Let's start by creating a new function called compareReports() just below our launchChromeAndRunLighthouse() , which will contain all of the comparison logic. Well give it two parameters from and to to accept the two reports used for comparison.

For now, as a placeholder, simply print out some data from each report to the console to validate that it is receiving it correctly.

const compareReports = (from, to) => {
  console.log(from("finalUrl") + " " + from("fetchTime"));
  console.log(to("finalUrl") + " " + to("fetchTime"));
};

As this comparison would start after the creation of a new report, the logic to execute this function should then function for launchChromeAndRunLighthouse().

If, for example, you have 30 reports in a directory, we need to determine which is the most recent and define it as the previous report to which the new will be compared. Fortunately, we have already decided to use a timestamp as the file name for a report, so this gives us something to work on.

First of all, we need to collect all the existing reports. To make this process easier, install a new dependency called glob, which allows model matching when searching for files. This is essential because we cannot predict how many reports will exist or how they will be called.

Install it like any other dependency:

npm install glob --save-dev

Then import it at the top of the file in the same way as usual:

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
const fs = require('fs');
const glob = require('glob');

Use well glob to collect all the reports in the directory, whose name we already know via the dirName variable. It is important to define your sync option for true because we don't want the execution of JavaScript to continue until we know how many other reports exist.

launchChromeAndRunLighthouse(argv.url).then(results => {
  const prevReports = glob(`${dirName}/*.json`, {
    sync: true
  });

  // et al

});

This process returns an array of paths. So if the report directory looked like this:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

… then the resulting table would look like this:

(
 'lukeharrison.dev/2020-01-31T10_18_12.648Z.json',
 'lukeharrison.dev/2020-01-31T10_18_24.110Z.json'
)

Because we can only make a comparison if a previous report exists, use this table as a condition for the comparison logic:

const prevReports = glob(`${dirName}/*.json`, {
  sync: true
});

if (prevReports.length) {
}

We have a list of report file paths and we need to compare their time stamped file names to determine which one is the most recent.

This means that we must first collect a list of all file names, cut all irrelevant data such as directory names and take care to replace the underscores (_) back with two points (:) to convert them back to valid dates. The easiest way to do this is to use path, another native Node.js module

const path = require('path');

Pass the way as an argument to his parse method, like this:

path.parse('lukeharrison.dev/2020-01-31T10_18_24.110Z.json');

Returns this useful object:

{
  root: '',
  dir: 'lukeharrison.dev',
  base: '2020-01-31T10_18_24.110Z.json',
  ext: '.json',
  name: '2020-01-31T10_18_24.110Z'
}

Therefore, to get a list of all the timestamp file names, we can do the following:

if (prevReports.length) {
  dates = ();
  for (report in prevReports) {
    dates.push(
      new Date(path.parse(prevReports(report)).name.replace(/_/g, ":"))
    );
  }
}

Which again if our directory looked like:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

Would result in:

(
 '2020-01-31T10:18:12.648Z',
 '2020-01-31T10:18:24.110Z'
)

One useful thing about dates is that they are inherently comparable by default:

const alpha = new Date('2020-01-31');
const bravo = new Date('2020-02-15');

console.log(alpha > bravo); // false
console.log(bravo > alpha); // true

So using a reduce , we can collapse our date table until only the most recent one remains:

dates = ();
for (report in prevReports) {
  dates.push(new Date(path.parse(prevReports(report)).name.replace(/_/g, ":")));
}
const max = dates.reduce(function(a, b) {
  return Math.max(a, b);
});

If you were to print the contents of max at the console, it would display a UNIX timestamp, so now we just have to add another line to convert our most recent date back to the correct ISO format:

const max = dates.reduce(function(a, b) {
 return Math.max(a, b);
});
const recentReport = new Date(max).toISOString();

Assuming this is the list of reports:

  • 2020-01-31T23_24_41.786Z.json
  • 2020-01-31T23_25_36.827Z.json
  • 2020-01-31T23_37_56.856Z.json
  • 2020-01-31T23_39_20.459Z.json
  • 2020-01-31T23_56_50.959Z.json

The value of recentReport would be 2020-01-31T23:56:50.959Z.

Now that we know the most recent report, we now need to extract its content. Create a new variable called recentReportContents Under the recentReport variable and assign it an empty function.

Since we know that this function will always have to be executed, rather than calling it manually, it makes sense to transform it into an IFFE (function expression immediately invoked), which will execute itself. even when the JavaScript parser reaches it. This is indicated by the additional parenthesis:

const recentReportContents = (() => {

})();

In this function, we can return the contents of the most recent report using the readFileSync() native method fs module. Because it will be in JSON format, it is important to parse it in a standard JavaScript object.

const recentReportContents = (() => {
  const output = fs.readFileSync(
    dirName + "https://css-tricks.com/" + recentReport.replace(/:/g, "_") + ".json",
    "utf8",
    (err, results) => {
      return results;
    }
  );
  return JSON.parse(output);
})();

And then, it's a matter of calling the compareReports() function and passing the current report and the most recent report as arguments.

compareReports(recentReportContents, results.js);

For now, just print a few details on the console so that we can test that the report data goes through OK:

https://www.lukeharrison.dev/ 2020-02-01T00:25:06.918Z
https://www.lukeharrison.dev/ 2020-02-01T00:25:42.169Z

If you get errors at this point, try removing all report.json files or reports without valid content from the start of the tutorial.

Compare two reports

The main remaining requirement was the ability to compare two reports from two websites. The simplest way to implement this would be to allow the user to pass the full report file paths as command line arguments which are then sent to the compareReports() a function.

In the command line, it would look like:

node lh.js --from lukeharrison.dev/2020-02-01T00:25:06.918Z --to cnn.com/2019-12-16T15:12:07.169Z

To achieve this, you must modify the conditional if which checks for the presence of a URL command line argument. Well add an extra check to see if the user just passed a from and to path, otherwise check the URL as before. In this way, prevent further checking of the headlight.

if (argv.from && argv.to) {

} else if (argv.url) {
 // et al
}

Allows you to extract the content of these JSON files, analyze them as JavaScript objects, then transmit them to the compareReports() a function.

We have already analyzed JSON before retrieving the most recent report. We can just extrapolate this functionality into its own helper function and use it in both locations.

Using the recentReportContents() operate as a base, create a new function called getContents() which accepts a file path as an argument. Make sure it is just a regular function, rather than an IFFE, as we don't want it to run as soon as the JavaScript parser finds it.

const getContents = pathStr => {
  const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
    return results;
  });
  return JSON.parse(output);
};

const compareReports = (from, to) => {
  console.log(from("finalUrl") + " " + from("fetchTime"));
  console.log(to("finalUrl") + " " + to("fetchTime"));
};

Then update the recentReportContents() to use this extrapolated help function instead:

const recentReportContents = getContents(dirName + "https://css-tricks.com/" + recentReport.replace(/:/g, '_') + '.json');

Back in our new conditional, we must transmit the content of the comparison reports to compareReports() a function.

if (argv.from && argv.to) {
  compareReports(
    getContents(argv.from + ".json"),
    getContents(argv.to + ".json")
  );
}

As before, this should print basic reporting information to the console to let us know that everything is working fine.

node lh.js --from lukeharrison.dev/2020-01-31T23_24_41.786Z --to lukeharrison.dev/2020-02-01T11_16_25.221Z

Would lead to:

https://www.lukeharrison.dev/ 2020-01-31T23_24_41.786Z
https://www.lukeharrison.dev/ 2020-02-01T11_16_25.221Z

Comparison logic

This part of the development consists in building a comparison logic to compare the two reports received by the compareReports() a function.

In the object returned by Lighthouse, there is a property called audits which contains another object that lists performance measures, opportunities, and information. There is a lot of information here, many of which we are not interested in for the purposes of this tool.

Here is the entry for First Contentful Paint, one of the nine performance measures we want to compare:

"first-contentful-paint": {
  "id": "first-contentful-paint",
  "title": "First Contentful Paint",
  "description": "First Contentful Paint marks the time at which the first text or image is painted. (Learn more)(https://web.dev/first-contentful-paint).",
  "score": 1,
  "scoreDisplayMode": "numeric",
  "numericValue": 1081.661,
  "displayValue": "1.1 s"
}

Create a table listing the keys to these nine performance measures. We can use it to filter the audit object:

const compareReports = (from, to) => {
  const metricFilter = (
    "first-contentful-paint",
    "first-meaningful-paint",
    "speed-index",
    "estimated-input-latency",
    "total-blocking-time",
    "max-potential-fid",
    "time-to-first-byte",
    "first-cpu-idle",
    "interactive"
  );
};

Then go through one of the reports audits object, then cross-reference its name to our filter list. (It doesn't matter which audit object, because they both have the same content structure.)

If it's there, then great, we want to use it.

const metricFilter = (
  "first-contentful-paint",
  "first-meaningful-paint",
  "speed-index",
  "estimated-input-latency",
  "total-blocking-time",
  "max-potential-fid",
  "time-to-first-byte",
  "first-cpu-idle",
  "interactive"
);

for (let auditObj in from("audits")) {
  if (metricFilter.includes(auditObj)) {
    console.log(auditObj);
  }
}

This console.log() would print the following keys on the console:

first-contentful-paint
first-meaningful-paint
speed-index
estimated-input-latency
total-blocking-time
max-potential-fid
time-to-first-byte
first-cpu-idle
interactive

Which means we would use from('audits')(auditObj).numericValue and to('audits')(auditObj).numericValue respectively in this loop to access the metrics themselves.

If we were to print them to the console with the key, it would give an output like this:

first-contentful-paint 1081.661 890.774
first-meaningful-paint 1081.661 954.774
speed-index 15576.70313351777 1098.622294504341
estimated-input-latency 12.8 12.8
total-blocking-time 59 31.5
max-potential-fid 153 102
time-to-first-byte 16.859999999999985 16.096000000000004
first-cpu-idle 1704.8490000000002 1918.774
interactive 2266.2835 2374.3615

We now have all the data we need. We just need to calculate the percentage difference between these two values ​​and then connect it to the console using the color coded format described above.

Do you know how to calculate the percentage change between two values? Neither do I. Fortunately, everyone's favorite monolithic search engine came to the rescue.

The formula is:

((From - To) / From) x 100

Suppose therefore that we have a speed index of 5.7 s for the first gear (from), then a value of 2.1 s for the second (to). The calculation would be:

5.7 - 2.1 = 3.6
3.6 / 5.7 = 0.63157895
0.63157895 * 100 = 63.157895

Rounding it to two decimal places would decrease the speed index by 63.16%.

Lets put this in a helper function inside the compareReports() function, under the metricFilter board.

const calcPercentageDiff = (from, to) => {
  const per = ((to - from) / from) * 100;
  return Math.round(per * 100) / 100;
};

Back in our auditObj conditional, we can start collecting the comparison output from the final report.

First, use the helper function to generate the percentage difference for each metric.

for (let auditObj in from("audits")) {
  if (metricFilter.includes(auditObj)) {
    const percentageDiff = calcPercentageDiff(
      from("audits")(auditObj).numericValue,
      to("audits")(auditObj).numericValue
    );
  }
}

Next, we need to display values ​​in this format on the console:

First Contentful Paint is 0.49% slower
First significant paint is 0.47% slower
The speed index is 12.92% slower
The estimated input latency is the same
85.71% faster total lockup time
The maximum potential entry delay is 10.53% faster
19.89% slower delay before first byte
First idle processor is 0.47% slower
The interactivity time is 0.02% slower

This requires adding color to the output of the console. In Node.js, this can be done by passing a color code as an argument to console.log() operate like this:

console.log('x1b(36m', 'hello') // Would print 'hello' in cyan

You can get a full color code reference in this Stackoverflow question. We need green and red, so that’s x1b(32m and x1b(31m respectively. For measurements where the value remains unchanged, simply use white. It would be x1b(37m.

Depending on whether the percentage increase is a positive or negative number, the following things must happen:

  • Log color should change (green for negative, red for positive, white for unchanged)
  • The content of the newspaper text changes.
    • (Name) is X% slower for positive numbers
    • (Name) is X% faster for negative numbers
    • (Name) is unchanged for numbers with no percentage difference.
  • If the number is negative, we want to remove the minus / negative symbol, otherwise, you would have a sentence like The speed index is -92.95% faster which doesn't make sense.

There are several ways to do this. Here, use the Math.sign() , which returns 1 if its argument is positive, 0 if 0 and -1 if the number is negative. It will do the trick.

for (let auditObj in from("audits")) {
  if (metricFilter.includes(auditObj)) {
    const percentageDiff = calcPercentageDiff(
      from("audits")(auditObj).numericValue,
      to("audits")(auditObj).numericValue
    );

    let logColor = "x1b(37m";
    const log = (() => {
      if (Math.sign(percentageDiff) === 1) {
        logColor = "x1b(31m";
        return `${percentageDiff + "%"} slower`;
      } else if (Math.sign(percentageDiff) === 0) {
        return "unchanged";
      } else {
        logColor = "x1b(32m";
        return `${percentageDiff + "%"} faster`;
      }
    })();
    console.log(logColor, `${from("audits")(auditObj).title} is ${log}`);
  }
}

So, we have it.

You can create new Lighthouse reports, and if a previous one exists, a comparison is made.

And you can also compare two reports from two sites.

Full source code

Here is the full source code for the tool, which you can also view in a Gist via the link below.

const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
const argv = require("yargs").argv;
const url = require("url");
const fs = require("fs");
const glob = require("glob");
const path = require("path");

const launchChromeAndRunLighthouse = url => {
  return chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    return lighthouse(url, opts).then(results => {
      return chrome.kill().then(() => {
        return {
          js: results.lhr,
          json: results.report
        };
      });
    });
  });
};

const getContents = pathStr => {
  const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
    return results;
  });
  return JSON.parse(output);
};

const compareReports = (from, to) => {
  const metricFilter = (
    "first-contentful-paint",
    "first-meaningful-paint",
    "speed-index",
    "estimated-input-latency",
    "total-blocking-time",
    "max-potential-fid",
    "time-to-first-byte",
    "first-cpu-idle",
    "interactive"
  );

  const calcPercentageDiff = (from, to) => {
    const per = ((to - from) / from) * 100;
    return Math.round(per * 100) / 100;
  };

  for (let auditObj in from("audits")) {
    if (metricFilter.includes(auditObj)) {
      const percentageDiff = calcPercentageDiff(
        from("audits")(auditObj).numericValue,
        to("audits")(auditObj).numericValue
      );

      let logColor = "x1b(37m";
      const log = (() => {
        if (Math.sign(percentageDiff) === 1) {
          logColor = "x1b(31m";
          return `${percentageDiff.toString().replace("-", "") + "%"} slower`;
        } else if (Math.sign(percentageDiff) === 0) {
          return "unchanged";
        } else {
          logColor = "x1b(32m";
          return `${percentageDiff.toString().replace("-", "") + "%"} faster`;
        }
      })();
      console.log(logColor, `${from("audits")(auditObj).title} is ${log}`);
    }
  }
};

if (argv.from && argv.to) {
  compareReports(
    getContents(argv.from + ".json"),
    getContents(argv.to + ".json")
  );
} else if (argv.url) {
  const urlObj = new URL(argv.url);
  let dirName = urlObj.host.replace("www.", "");
  if (urlObj.pathname !== "https://css-tricks.com/") {
    dirName = dirName + urlObj.pathname.replace(///g, "_");
  }

  if (!fs.existsSync(dirName)) {
    fs.mkdirSync(dirName);
  }

  launchChromeAndRunLighthouse(argv.url).then(results => {
    const prevReports = glob(`${dirName}/*.json`, {
      sync: true
    });

    if (prevReports.length) {
      dates = ();
      for (report in prevReports) {
        dates.push(
          new Date(path.parse(prevReports(report)).name.replace(/_/g, ":"))
        );
      }
      const max = dates.reduce(function(a, b) {
        return Math.max(a, b);
      });
      const recentReport = new Date(max).toISOString();

      const recentReportContents = getContents(
        dirName + "https://css-tricks.com/" + recentReport.replace(/:/g, "_") + ".json"
      );

      compareReports(recentReportContents, results.js);
    }

    fs.writeFile(
      `${dirName}/${results.js("fetchTime").replace(/:/g, "_")}.json`,
      results.json,
      err => {
        if (err) throw err;
      }
    );
  });
} else {
  throw "You haven't passed a URL to Lighthouse";
}

See Gist

Next steps

With the completion of this basic Google Lighthouse tool, there are many ways to develop it further. For example:

  • Une sorte de tableau de bord en ligne simple qui permet aux utilisateurs non techniques d'exécuter des audits Lighthouse et de voir les métriques se développer au fil du temps. Il peut être difficile d'amener les parties prenantes à la performance Web, donc quelque chose de tangible qu'elles peuvent intéresser pourrait piquer leur intérêt.
  • Développez la prise en charge des budgets de performances. Par conséquent, si un rapport est généré et que les mesures de performances sont plus lentes qu'elles ne le devraient, l'outil génère des conseils utiles sur la façon de les améliorer (ou vous appelle des noms).

Bonne chance!

What Are The Main Benefits Of Comparing Car Insurance Quotes Online

LOS ANGELES, CA / ACCESSWIRE / June 24, 2020, / Compare-autoinsurance.Org has launched a new blog post that presents the main benefits of comparing multiple car insurance quotes. For more info and free online quotes, please visit https://compare-autoinsurance.Org/the-advantages-of-comparing-prices-with-car-insurance-quotes-online/ The modern society has numerous technological advantages. One important advantage is the speed at which information is sent and received. With the help of the internet, the shopping habits of many persons have drastically changed. The car insurance industry hasn't remained untouched by these changes. On the internet, drivers can compare insurance prices and find out which sellers have the best offers. View photos The advantages of comparing online car insurance quotes are the following: Online quotes can be obtained from anywhere and at any time. Unlike physical insurance agencies, websites don't have a specific schedule and they are available at any time. Drivers that have busy working schedules, can compare quotes from anywhere and at any time, even at midnight. Multiple choices. Almost all insurance providers, no matter if they are well-known brands or just local insurers, have an online presence. Online quotes will allow policyholders the chance to discover multiple insurance companies and check their prices. Drivers are no longer required to get quotes from just a few known insurance companies. Also, local and regional insurers can provide lower insurance rates for the same services. Accurate insurance estimates. Online quotes can only be accurate if the customers provide accurate and real info about their car models and driving history. Lying about past driving incidents can make the price estimates to be lower, but when dealing with an insurance company lying to them is useless. Usually, insurance companies will do research about a potential customer before granting him coverage. Online quotes can be sorted easily. Although drivers are recommended to not choose a policy just based on its price, drivers can easily sort quotes by insurance price. Using brokerage websites will allow drivers to get quotes from multiple insurers, thus making the comparison faster and easier. For additional info, money-saving tips, and free car insurance quotes, visit https://compare-autoinsurance.Org/ Compare-autoinsurance.Org is an online provider of life, home, health, and auto insurance quotes. This website is unique because it does not simply stick to one kind of insurance provider, but brings the clients the best deals from many different online insurance carriers. In this way, clients have access to offers from multiple carriers all in one place: this website. On this site, customers have access to quotes for insurance plans from various agencies, such as local or nationwide agencies, brand names insurance companies, etc. "Online quotes can easily help drivers obtain better car insurance deals. All they have to do is to complete an online form with accurate and real info, then compare prices", said Russell Rabichev, Marketing Director of Internet Marketing Company. CONTACT: Company Name: Internet Marketing CompanyPerson for contact Name: Gurgu CPhone Number: (818) 359-3898Email: [email protected]: https://compare-autoinsurance.Org/ SOURCE: Compare-autoinsurance.Org View source version on accesswire.Com:https://www.Accesswire.Com/595055/What-Are-The-Main-Benefits-Of-Comparing-Car-Insurance-Quotes-Online View photos



picture credit

ExBUlletin

to request, modification Contact us at Here or [email protected]

Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *