Large table mousehover effects
The problem with the current approach, as I'm sure you already know, is the very high number of DOM elements that the JavaScript engine is navigating through and assessing before the end of the process (bearing the 30,000 row scenario in mind).

And the jQuery selector function has a fair bit of work to do, checking every row element to see if it has the data attribute value or not, before it can even begin the highlighting. A taxing process. DOM manipulation is notoriously hard on performance.

My answer would be that the mapping between row and parent, which is what essentially needs to be calculated for each row when hovered, needs to be taken out of the DOM entirely so this calculation doesn't require any work with the DOM at all.

See my solution below, and this JSFiddle: https://jsfiddle.net/dawdg4sj/4

Ultimately, you'll see that on each row hover, the 'calculateHighlighting' function is called which performs the calculation "off-line" so to speak, away from the DOM (data attributes withdrawn) and instead uses the local 'map' object (row IDs on the left, parent row IDs on the right), and returned is a straight-forward array of objects each describing an element by ID and a class to add for highlighting purposes.

Bottom line, you need to be able to generate the row id/parent mapping outside the DOM and inside JavaScript instead. As long as you have this mapping information inside the DOM and wish to process 30,000 rows, it'll be a sluggish affair.

(Note: My solution uses function calls from the Lodash library for convenience, i.e. '.keys(.pick(...'. They are optional, keep or swap.)

Finally, and separately, I would express my surprise at such a large number of rows. Working with so many elements in a single view of an app via JavaScript is always going to lead to slow processing to one extent or another. But obviously so many rows could not be visible within the viewport all at once, so I would further suggest that part of the solution is to limit the highlighting to those within view and just outside only, and update on scroll, or even paginate the rows. It depends on the context of the project of course, but those are my supplementary thoughts.)

// ---
// New-look HTML
// ---

&lt;table&gt;
    &lt;tr id="row-1"&gt;
        &lt;td&gt;test&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id="row-2"&gt;
        &lt;td&gt;tes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id="row-3"&gt;
        &lt;td&gt;tes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id="row-4"&gt;
        &lt;td&gt;test&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id="row-5"&gt;
        &lt;td&gt;tes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id="row-6"&gt;
        &lt;td&gt;tes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id="row-7"&gt;
        &lt;td&gt;test&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;


// ---
// New-look JS
// ---

var map = { // row id -&gt; parent row id
    1: 0,
    2: 1,
    3: 1,
    4: 3,
    5: 3,
    6: 1,
    7: 0
};
var highlightColours = 4;

// calculateHighlighting
// Description: Establish a straight-forward array of highlighting rules (classes to add to elements)
// e.g. [ {element: '#row-1', class: 'row-colour-1'}, {element: '#row-2', class: 'row-colour-4'} ... ]

function calculateHighlighting(rootElementID) {
    var result = [];
    var elementIDPrefix = '#row-';
    var elementColourClassPrefix = 'row-colour-';

    var parentRows = [];
    var id = rootElementID.split('-')[1]; // (e.g. '#row-1' -&gt; 1)
    parentRows.push(id); // Start with the root (hovered) element

    var colourIndex = 1; // Keep track of highlight colours used

    while(parentRows.length &gt; 0 || colourIndex &lt;= highlightColours) { // While there are parent rows to highlight (or we run out of highlight colours)
        var childRows;
        for(var i = 0; i &lt; parentRows.length; i++) {
            // Fetch related rows based on id/parent mapping (using lodash function, _.keys, for convenience)
            childRows = _.keys(_.pick(map, function(parentID) {
                return parentID == parentRows[i];
            }));
            if(childRows.length &gt; 0) { // Highlight found parent rows
                for(var j = 0; j &lt; childRows.length; j++) {
                    result.push({
                        element: elementIDPrefix + childRows[j],
                        class: elementColourClassPrefix + colourIndex
                    });
                }
            }
        }
        parentRows = childRows; // Just highlighted parent rows, become base for next parent search
        colourIndex++; // Next highlight colour
    }

    return result;
}

$('tr').on('mouseover', function () {
    var rowID = $(this).attr('id');
    var rowsToHighlight = calculateHighlighting(rowID);
    for(var i = 0; i &lt; rowsToHighlight.length; i++) {
        $(rowsToHighlight[i].element).addClass(rowsToHighlight[i].class);
    }
});

$('tr').on('mouseleave', function () {
    $('tr').attr('class', '');
});


// ---
// New-look CSS
// ---
tr:hover {
    background-color: red;
}
.row-colour-1 {
    background-color: #1ba1e2;
}
.row-colour-2 {
    background-color: #75F7FF;
}
.row-colour-3 {
    background-color: #98E8E3;
}
.row-colour-4 {
    background-color: #B6DCE8;
}