73

The Coverage tool is good at finding used and unused code. However, there doesn't appear to be a way to save or export only the used code. Even hiding unused code would be helpful.

I'm attempting to reduce the amount of Bootstrap CSS in my application; the file is more than 7000 lines. The only way to get just the used code is to carefully scroll thru the file, look for green sections, then copy that code to a new file. It's time-consuming and unreliable.

Is there a different way? Chrome 60 does not seem to have added this functionality.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Grid Trekkor
  • 1,443
  • 2
  • 14
  • 19
  • This question [Using the Code Coverage devtool by selenium+maven automation](https://stackoverflow.com/questions/43591829/) seems related. – surfmuggle Oct 16 '17 at 22:03
  • Regarding is there a better option. This [mdn page on css coverage](https://developer.mozilla.org/en-US/docs/Tools/CSS_Coverage) has the **warning `This feature is experimental and is not yet available in Firefox.`** In Firefox ESR 45.8.0 it can be activated by pressing `SHIFT F2` which opens the GCLI [Graphical Command Line Interpreter](https://developer.mozilla.org/en-US/docs/Tools/GCLI) Christian Heilmann uploaded a [video CSSCoverage tool in Firefox Devtools](https://www.youtube.com/watch?v=Qrzf1lsBMMw) – surfmuggle Oct 17 '17 at 09:01
  • I bet you could write a chrome extension to export the data – Dale Oct 23 '17 at 20:54
  • Great question, same issue here – Nathan B Jun 13 '21 at 14:29
  • 1
    The feature @surfmuggle mentioned is unfortunately gone now -- https://support.mozilla.org/en-US/questions/1248832 – hostingutilities.com Aug 26 '21 at 03:54

11 Answers11

23

You can do this with puppeteer

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage()

  //Start sending raw DevTools Protocol commands are sent using `client.send()`
  //First off enable the necessary "Domains" for the DevTools commands we care about
  const client = await page.target().createCDPSession()
  await client.send('Page.enable')
  await client.send('DOM.enable')
  await client.send('CSS.enable')

  const inlineStylesheetIndex = new Set();
  client.on('CSS.styleSheetAdded', stylesheet => {
    const { header } = stylesheet
    if (header.isInline || header.sourceURL === '' || header.sourceURL.startsWith('blob:')) {
      inlineStylesheetIndex.add(header.styleSheetId);
    }
  });

  //Start tracking CSS coverage
  await client.send('CSS.startRuleUsageTracking')

  await page.goto(`http://localhost`)
  // const content = await page.content();
  // console.log(content);

  const rules = await client.send('CSS.takeCoverageDelta')
  const usedRules = rules.coverage.filter(rule => {
    return rule.used
  })

  const slices = [];
  for (const usedRule of usedRules) {
    // console.log(usedRule.styleSheetId)
    if (inlineStylesheetIndex.has(usedRule.styleSheetId)) {
      continue;
    }

    const stylesheet = await client.send('CSS.getStyleSheetText', {
      styleSheetId: usedRule.styleSheetId
    });

    slices.push(stylesheet.text.slice(usedRule.startOffset, usedRule.endOffset));
  }

  console.log(slices.join(''));

  await page.close();
  await browser.close();
})();
Chuck Le Butt
  • 47,570
  • 62
  • 203
  • 289
stereobooster
  • 460
  • 4
  • 11
  • Which version of `node` are you using to run your solution? – dryleaf Apr 12 '18 at 05:54
  • 1
    @dryleaf nodejs 8.6 – stereobooster Apr 12 '18 at 21:25
  • 1
    Thanks. I was able to run on 8.0.0 after seeing the puppeteer documentation. That's a nice CSS hack. +1 for giving a great answer. How would I go about a JS hack? – dryleaf Apr 12 '18 at 21:32
  • I guess we can `filter` and get the unused rules too, to have they both separately and load the unused rules as deferred styles. Maybe they are used in other internal pages. – JCarlosR Dec 23 '18 at 18:21
  • What if I just want to concentrate on bootstrap.css and leave all others as they were? How would I need to change the coding above? – HHeckner Mar 01 '21 at 07:15
16

You can do this with Headless Chrome and puppeteer:

  1. In a new folder install puppeteer using npm (this will include Headless Chrome for you):

npm i puppeteer --save

  1. Put the following in a file called csscoverage.js and change localhost to point to your website.

:

const puppeteer = require('puppeteer');
const util = require('util');
const fs    = require("fs");

(async () => {
 const browser = await puppeteer.launch();
 const page = await browser.newPage();
 await page.coverage.startCSSCoverage();
 await page.goto('https://localhost'); // Change this
 const css_coverage = await page.coverage.stopCSSCoverage();
 console.log(util.inspect(css_coverage, { showHidden: false, depth: null }));
 await browser.close();

let final_css_bytes = '';
let total_bytes = 0;
let used_bytes = 0;

for (const entry of css_coverage) {
  final_css_bytes = "";

  total_bytes += entry.text.length;
  for (const range of entry.ranges) {
    used_bytes += range.end - range.start - 1;
    final_css_bytes += entry.text.slice(range.start, range.end) + '\n';
  }

  filename = entry.url.split('/').pop();

  fs.writeFile('./'+filename, final_css_bytes, error => {
    if (error) {
      console.log('Error creating file:', error);
    } else {
      console.log('File saved');
    }
  });
}
})();
  1. Run it with node csscoverage.js

This will output all the CSS you're using into the separate files they appear in (stopping you from merging external libraries into your own code, like the other answer does).

Chuck Le Butt
  • 47,570
  • 62
  • 203
  • 289
  • 1
    Add await page.setDefaultNavigationTimeout(0); to avoid getting timeout when running for a heavy load or a page that loads harder – 1istbesser Mar 19 '20 at 21:44
  • 2
    This is only extracting the mobile css. Somehow bigger screen css is not getting extracted. Any idea how to fix this? – Erenn May 19 '22 at 11:28
  • there are some problems with this code: - if multiple CSS files with the same name but with a different path are referenced (as can happen eg with some CSS merging plugins) it will overwrite them all over each other and save only the last file. - in case a URL in `coverage` ends with slash, it will try to create a file with an empty filename. – gilad905 Sep 09 '22 at 10:31
10

I talked with the engineer who owns this feature. As of Chrome 64 there's still no way to export the results of a coverage analysis.

Star issue #717195 to help the team prioritize this feature request.

Kayce Basques
  • 23,849
  • 11
  • 86
  • 120
10

I love this simple solution. It works with the Coverage tool in Chrome without any further installation. You can simply use the json file that the Coverage tool lets you export:

https://nachovz.github.io/devtools-coverage-css-generator/

But be aware of the comment below my answer!!! He is right, it's risky. I am still hoping / waiting for an update.

tmighty
  • 10,734
  • 21
  • 104
  • 218
7
  1. first of all you need to download and install "Google Chrome Dev".
  2. on Google chrome Dev go to Inspect element > Sources > Ctrl+shift+p
  3. Enter "coverage" and select "Start Instrumenting coverage and reload Page"
  4. Then use Export icon enter image description here this will give you a json file.

you can also visit : Chrome DevTools: Export your raw Code Coverage Data

reza jafari
  • 1,228
  • 13
  • 14
  • 2
    If you need to get only the coverage text for a file from the json, you can do it with this js code: `var file = json[N]; var coverage_file = ""; file.ranges.forEach(range => coverage_file += file.text.substring(range.start, range.end));`. Where `N` is the number of file in the json – Federico G Nov 12 '19 at 19:54
  • What you mean with "download and install "Google Chrome Dev" ? F12 is not enough ? – Omiod Nov 20 '19 at 09:31
  • Google Chrome Dev != Google Chrome. i think download coverage text is only available on Google Chrome Dev. Google chrome Dev is a version of chrome was built for developers. – reza jafari Nov 24 '19 at 07:48
  • 1
    Not sure when it was added, but this option is in the most recent (mac) version of Chrome as of me typing this - Version 89.0.4389.90 (Official Build) (x86_64) – Giovanni S Mar 23 '21 at 14:25
5

I downloaded the latest version of canary and the export button was present.

I then used this PHP script to parse the json file returned. (Where key '6' in the array is the resource to parse). I hope it helps someone!

$a = json_decode(file_get_contents('coverage3.json'));
$sText = $a[6]->text;
$sOut = "";
foreach ($a[6]->ranges as $iPos => $oR) {
    $sOut .= substr($sText, $oR->start, ($oR->end-$oR->start))." \n";
}
echo '<style rel="stylesheet" type="text/css">' . $sOut . '</style>';
atoms
  • 2,993
  • 2
  • 22
  • 43
  • 1
    Big thanks for this! However you have a small mistake there (or maybe the coverage.json was generated differently back then), the third parameter of the substr should be the length, but in the coverage.json the ending character is presented, so it should be: $sOut .= substr($sText, $oR->start, ($oR->end-$oR->start)); – joe007 May 30 '19 at 11:54
  • 1
    Also, you might want to use mb_substr() instead of substr() to account for multi-byte characters (sometimes used for icons in CSS). – Tomi Jun 08 '20 at 02:27
4

Chrome canary 73 can do it. You will need Windows or Mac OS. There is an export function (Down arrow icon) next to the record and clear buttons. You'll get a json file and then you can use that to programmatically remove the unused lines.

8oh8
  • 1,247
  • 5
  • 19
  • 35
2

Here's a version that will keep media queries, based on Christopher Schiefer's:

$jsont = <<<'EOD'
{ "url":"test"} 
EOD;

$a = json_decode($jsont);
$sText = $a->text;
preg_match_all('(@media(?>[^{]|(?0))*?{)', $sText, $mediaStartsTmp, PREG_OFFSET_CAPTURE);
preg_match_all("/\}(\s|\\n|\\t)*\}/", $sText, $mediaEndsTmp, PREG_OFFSET_CAPTURE);
$mediaStarts = empty($mediaStartsTmp) ? array() : $mediaStartsTmp[0];
$mediaEnds = empty($mediaEndsTmp) ? array() : $mediaEndsTmp[0];
$sOut = "";
$needMediaClose = false;
foreach ($a->ranges as $iPos => $oR) {
    if ($needMediaClose) { //you are in a media query
        //add closing bracket if you were in a media query and are past it
        if ($oR->start > $mediaEnds[0][1]) {
            $sOut .= "}\n";
            array_splice($mediaEnds, 0, 1);
            $needMediaClose = false;
        }
    }
    if (!$needMediaClose) {
        //remove any skipped media queries
        while (!empty($mediaEnds) && $oR->start > $mediaEnds[0][1]) {
            array_splice($mediaStarts, 0, 1);
            array_splice($mediaEnds, 0, 1);
        }
    }
    if (!empty($mediaStarts) && $oR->start > $mediaStarts[0][1]) {
       $sOut .= "\n" . $mediaStarts[0][0] . "\n";
       array_splice($mediaStarts, 0, 1);
       $needMediaClose = true;
    }
    $sOut .= mb_substr($sText, $oR->start, ($oR->end-$oR->start))." \n";
}
if ($needMediaClose) { $sOut .= '}'; }
echo '<style rel="stylesheet" type="text/css">' . $sOut . '</style>';
Phil W.
  • 81
  • 5
2

That's my python code to extract the code:

import json

code_coverage_filename = 'Coverage-20210613T173016.json'
specific_file_url = 'https://localhost:3000/b.css'

with open(code_coverage_filename) as f:
  data = json.load(f)
  
for entry in data:
    pass # print entry['url']
    if entry['url'] == specific_file_url:
        text = ""
        for range in entry['ranges']:
            range_start = range['start']
            range_end = range['end']
            text += entry['text'][int(range_start):int(range_end)]+"\n"
                
        print text

However, there is a problem. Chrome debugger doesn't mark these kind of lines

@media (min-width: 768px) {

So it's a bit problematic to use this technique

Nathan B
  • 1,625
  • 1
  • 17
  • 15
0

More practical version based on Atoms.

Improved to work without any files.

PHP Sandbox http://sandbox.onlinephpfunctions.com/

JSON Formater to be converted to 1line https://www.freeformatter.com/json-formatter.html#ad-output

Unmify it https://unminify.com/

$jsont = <<<'EOD'
{ "url":"test"} 
EOD;

$a = json_decode($jsont);
$sText = $a->text;
$sOut = "";
foreach ($a->ranges as $iPos => $oR) {
    $sOut .= substr($sText, $oR->start, ($oR->end-$oR->start))." \n";
}
echo '<style rel="stylesheet" type="text/css">' . $sOut . '</style>';
0

I use this DisCoverage chrome extension, it parses json file from coverage tool

  • Such a shame that it was removed from Google Chrome Extension store but it still available to download from https://chrome-stats.com/d/bagpgeanndnifdbdacmckipibgkbkpdp/download - there is a instruction how to install it as well. – Eryk Wróbel Mar 14 '23 at 08:15